Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 4 of 26

Easy Primitives

impl blocks: associated functions, methods, Self and associated constants

Build a counter type and drive it from a list of opcodes.

pub fn run_counter(ops: Vec<String>) -> i64

The counter starts at 0. "bump" adds STEP (which is 3). "reset" puts it back to the state a freshly built counter is in. Any other string is ignored. Return the final value.

The starter gives you struct Counter(i64) and Counter::new(); you supply the associated constant STEP and the methods bump, reset and get. It does not compile until you do — E0599, “no method named bump found”.

Rust has no classes

It has types, and separately it has impl blocks that hang functions off a type. Nothing in the language corresponds to a class body, and nothing corresponds to a constructor.

impl Counter {
    const STEP: i64 = 3;

    fn new() -> Self { Self(0) }        // associated function

    fn bump(&mut self) { self.0 += Self::STEP; }   // method

    fn get(&self) -> i64 { self.0 }     // method
}

Two kinds of thing live in there, and the difference is one word:

  • An associated function has no self parameter. You call it with ::, as in Counter::new(). new is just a name Rust programmers agreed on. It has no special status, no keyword, no compiler magic. You can call it build, with_capacity, from_parts, or have five of them.
  • A method takes self, &self or &mut self first. You call it with ., as in counter.bump().

Once that lands, a lot of transplanted object-oriented confusion evaporates. There is no this. There is no implicit receiver. Counter::new() is an ordinary function that happens to be namespaced under a type.

Which self?

  • &self — I will read you. The caller keeps the value and can use it after.
  • &mut self — I will change you. The caller keeps it, but nothing else may touch it while I have it.
  • self — I am consuming you. The caller cannot use it afterwards.

Choosing the weakest one that works is not politeness; it is what keeps your API usable. get should be &self, not self.

Self with a capital S

Inside an impl Counter, Self is an alias for Counter. Use it. It is shorter, it survives a rename, and it is what new() -> Self means. The use_self lint exists to push you toward it.

It works in expression position too: Self(0) builds a Counter, and Self::STEP reads the associated constant. And because reset has &mut self, the tidiest body is *self = Self::new(); — assign a whole fresh value through the mutable reference, rather than poking fields one at a time.

Associated constants

const STEP: i64 = 3; inside the impl block belongs to the type, not to any value. Counter::STEP works from outside; Self::STEP from inside. There is no instance storage — it is a compile-time constant with a namespace, and it is the right home for a magic number that only means something in the context of this type.

Two lints that will bite you eventually

new_without_default says: if you offer a public pub fn new() -> Self taking no arguments, you should also implement Default, because generic code asks for Default and cannot ask for new. The rule is narrow, and worth knowing precisely: it is gated on visibility and arity. A private fn new() does not trigger it — which is why the new here does not — and neither does pub fn new(x: i64, y: i64).

wrong_self_convention enforces the naming convention the standard library uses: to_* borrows (&self), into_* consumes (self), as_* is a cheap borrowing view, is_* and has_* answer questions about &self. Name a method to_u32 and take self by value on a non-Copy type and clippy will stop you.

Remember the grade is compile + tests + clippy -D warnings.