Choosing a Receiver: self, &self, or &mut self
You have a struct from Structs and Field Syntax and a handful of functions that take one as their first argument. The impl block is where those functions become methods — associated with the type, callable with dot syntax, and — this is the part that matters — borrowing-checked at every call site:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
fn increment(&mut self) {
self.count += 1;
}
fn value(&self) -> u32 {
self.count
}
fn reset(self) -> Self {
Counter { count: 0 }
}
}
Read that block as four different contracts in one file. new produces a value without any instance to borrow — an associated function, called Counter::new(). increment mutates through &mut self. value reads through &self. reset takes self by move and hands back a fresh Self, retiring the original. The struct definition itself declares no behavior; the impl block is the sole place behavior attaches, and there is no inheritance layer between them.
Every method's first parameter is a receiver, and its form is the method's contract with the borrow checker. &self borrows immutably: the method may read fields, and any number of callers may hold &Counter while it runs. &mut self borrows mutably: the method may write fields, but only one such borrow can be live — a concurrent &counter elsewhere in scope is a compile error (error[E0502]: cannot borrow 'counter' as immutable because it is also borrowed as mutable). self moves the value into the method: the caller's binding is dead afterward, and the method may consume, transform, or re-box the struct. That form is less common but load-bearing for builder patterns that consume self at the end of a chain.
Rust inserts the receiver sugar automatically at every call site. Writing counter.increment() is rewritten by the compiler to the equivalent of Counter::increment(&mut counter) — auto-ref — which is why calling a &mut self method on a plain immutable binding usually produces an error naming the binding, not the method. The fix is almost always to make the binding mutable (let mut counter = ...) or to end the other borrow first. Note what does not happen: &self methods do not copy the struct. The borrow is temporary, lasting only the call, and the value is untouched afterward — several value() calls in sequence are several independent shared borrows, never duplicates. The consuming form shows up most often in fluent builders:
impl Query {
fn filter(mut self, f: String) -> Self {
self.filters.push(f);
self
}
}
let q = Query { filters: vec![] }
.filter("active".into())
.filter("admin".into()); // each call consumes and returns; q owns the result
Each filter call moves the Query in, mutates the local copy, and returns it — the caller never sees an intermediate half-built value. The practical upshot: the receiver you write in the impl block determines what callers can do and when, so choosing it is an API decision, not an implementation detail. Prefer &self, reach for &mut self when mutation is the point, and reserve self for methods that genuinely consume. A method that takes self by value and returns Self is telling every caller "this produces a new thing and the old one is finished," which is exactly the semantics a consuming builder should express.
Associated Functions, Constructors, and the Self Keyword
Drop the receiver and the function is still associated with the type, but it is not a method — you call it with double-colon syntax on the type itself:
impl Counter {
fn from_start(start: u32) -> Self {
Counter { count: start }
}
}
let c = Counter::from_start(10); // Counter::, not a dot — there is no instance yet
The canonical example everyone already uses is String::new() and String::with_capacity(64): constructors that produce a value of the type, called before any instance exists. The convention of returning Self — the implementing type — is deliberate. Self in an inherent impl block is an alias for the type being implemented, and using it in return positions makes the signature survive a rename of the struct: rename Counter to EventCounter and every -> Self stays correct, while every -> Counter needs a find-and-replace. Interviewers sometimes ask "why not a constructor function outside the block?" — the answer is namespacing: Counter::from_start cannot collide with Registry::from_start, while two free functions named from_start in the same module can, and neither has a natural home the way an associated function does.
Self also appears inside the block wherever the type name would be boilerplate — struct literals, further calls, and (later) trait impls. The pattern below is worth memorizing because you will read it in every nontrivial crate:
impl Counter {
fn zeroed() -> Self {
Self { count: 0 } // Self { ... } == Counter { ... }
}
fn plus(&self, n: u32) -> Self {
Self { count: self.count + n }
}
}
Notice plus takes &self but returns a new Self: it reads the current value without consuming it and hands back an independent copy with the addition applied. That receiver-and-return combination — borrow in, new value out — is the signature of a non-consuming transformation, and reading it off a signature is a skill worth having. Contrast it with the consuming variant fn plus(self, n: u32) -> Self, which moves the original in; callers see the difference immediately, because using the old binding afterward is an E0382 error in one case and perfectly legal in the other.
Two distinctions keep this straight in interviews and in code. Inherent methods — those in an impl Type { } block with no trait — always win method resolution and are found first; they are where type-specific behavior belongs. Associated functions (no receiver) cannot be called with dot syntax — counter.zeroed() is an error — while methods can always be called with double-colon syntax too (Counter::value(&counter) is legal, just unusual). One more piece of syntax worth knowing: impl Default for Counter (a trait implementation) and impl Counter (an inherent block) look similar but serve different purposes — inherent blocks add behavior only this type has; trait blocks satisfy a shared contract. The for keyword is the tell: nothing on the left of a trait impl's for except the type being implemented. The next topic picks up that thread directly.
Multiple impl Blocks: Organization, Generics, and Conditional Compilation
A type may have more than one inherent impl block, and there are three good reasons to use that freedom.
Organization. As a type grows, keeping every method in one block makes files unreviewable. Splitting by concern — construction in one block, transformation in another, rendering in another — lets you navigate by impl header the way you navigate by class in other languages. The compiler does not care about the split; method resolution simply unions all inherent blocks. Real crates use this aggressively: the standard library's slice type [T], for instance, has one impl<T> [T] block for the methods that work on every element type and a separate bounded impl<T: Clone> [T] block for methods like to_vec that need to duplicate elements — the split is exactly the generic/bounded pattern below, applied to one of the most-used types in the language.
Generics. An impl<T> block attaches methods to every instantiation of a generic struct:
struct Registry<T> {
items: Vec<T>,
}
impl<T> Registry<T> {
fn new() -> Self { Registry { items: Vec::new() } }
fn push(&mut self, item: T) { self.items.push(item); }
fn len(&self) -> usize { self.items.len() }
}
impl<T: Clone> Registry<T> {
fn duplicate_last(&self) -> Option<T> {
self.items.last().cloned()
}
}
The second block is bounded: duplicate_last only exists for T that implement Clone, because it calls .cloned(). That is the seed of Generic Trait Bounds: the impl header is where the requirement is declared, and the method body may only use what the bound guarantees.
Conditional compilation gates whole blocks on build flags — a well-known pattern for platform-specific methods and for test-only helpers:
impl Connection {
fn open(addr: &str) -> Self { /* portable path */ }
#[cfg(target_os = "linux")]
fn set_tcp_fast_open(&mut self, _n: u32) { /* linux-only */ }
}
On non-Linux targets the gated method does not exist at all, so calling it is the ordinary "no method found" error rather than a link failure. The gate applies to the whole fn — signature and body alike — so there is no dead code left behind.
One hard rule constrains all of this: within a single type, no two inherent methods may share a name, regardless of which impl block declares them. Declaring fn push twice fails with error[E0592]: duplicate definitions with name 'push' — Rust does not dispatch on receiver types the way some languages do, so the method name must be unique per type. Organize, bound, and gate freely; just never collide.
Conditional compilation earns its place in day-to-day work beyond exotic platform code. A #[cfg(test)] impl block is the idiomatic home for test-only helpers: methods that build fixtures or assert invariants exist during cargo test and are compiled out of release builds entirely, with zero runtime cost and no dead-code warnings in production. The same attribute on a single method — not a whole block — works too, which keeps platform-specific behavior discoverable next to the portable path instead of exiled to another file. With methods and associated functions now solid, the next topic adds the second half of the story: shared interfaces implemented across many types.
Quiz
1. A method is declared as `fn log_state(&self)`. At the call site `obj.log_state()`, what does the compiler effectively insert?
2. What distinguishes an associated function from a method, and how do you call each?
3. In an impl block for EventCounter, what does `-> Self` in a signature mean, and why is it preferred over naming the struct?
4. You add a second `impl Registry` block declaring `fn push(&mut self, item: T)`, but the type already has that method. What happens?
Flashcards
Question
What are the three receiver forms, and what does each allow and cost?
Click to reveal answer
Answer
&self: immutable borrow — reads fields, any number of concurrent &T borrows allowed. &mut self: mutable borrow — writes fields, but only one live borrow of the value (E0502 if a &T is also held). self: moves the value into the method — caller's binding is dead afterward; used for consuming builders. The receiver is the method's contract with the borrow checker.
Question
What is auto-ref at a method call site?
Click to reveal answer
Answer
The compiler rewrites `obj.method()` into the equivalent of Type::method(&obj) or Type::method(&mut obj) depending on the receiver, inserting the borrow implicitly. Consequence: calling a &mut self method on an immutable binding errors with E0502 naming the binding, and you fix it by making the binding mutable or by holding no conflicting borrows.
Question
What is an associated function, when is it the right choice, and how do you call it?
Click to reveal answer
Answer
An fn in an impl block with no self receiver. Call it with double-colon syntax on the type: Counter::from_start(10), String::new(). Use it primarily for constructors that produce a value before any instance exists. Return Self so the signature survives renaming. It cannot be called with dot syntax — there is no instance to borrow from.
Question
What does Self mean in an impl block, and where is it legal?
Click to reveal answer
Answer
Self is an alias for the type the impl block is attached to — legal in both inherent and trait impls, in return types (-> Self), struct literals (Self { field }), and nested calls. It makes signatures rename-proof. In trait impls it also refers to the implementing type, which is how one body serves whatever type the impl is written for.
Question
Name the three reasons to use multiple impl blocks for one type, and the one naming rule that constrains them.
Click to reveal answer
Answer
Reasons: organization (split construction, transformation, rendering into separate blocks), generics (impl<T> and bounded impl<T: Trait> blocks attach methods only to qualifying instantiations), and conditional compilation (#[cfg(target_os = "linux")] gates a block so its methods only exist on that platform). Constraint: no two inherent methods for a type may share a name — E0592 duplicate definitions — regardless of which block declares them.
Revision Notes
Key Takeaways
- 1. Methods live in impl blocks whose first parameter is a receiver; the receiver's form (self / &self / &mut self) is the method's borrow-checker contract, not a detail
- 2. Auto-ref rewrites obj.m() into Type::m(&obj) — which is why calling a &mut self method on an immutable binding errors with E0502 naming the binding
- 3. Associated functions have no receiver and use Type::name() syntax — the constructor pattern; returning Self keeps signatures correct across renames
- 4. Multiple impl blocks per type are legal and useful for organization, bounded generics (impl<T: Clone>), and #[cfg(...)] conditional methods
- 5. Inherent method names must be unique per type across all impl blocks — a second push() is E0592, not an overload; trait impls provide the shared-interface alternative
Interview Tips
- • "What are the receiver options and when do you use each?" — &self (read-only, most common), &mut self (mutation is the point), self (consuming builder); add that the receiver determines what callers can do, so it is an API decision
- • "Explain auto-ref at method calls." — obj.m() becomes Type::m(&obj)/(&mut obj); use it to explain why E0502 surfaces at the *call site* and how making the binding mutable or ending the other borrow fixes it
- • "Difference between an inherent method and a trait method?" — inherent impls (impl Type) are type-specific and always resolve first; trait impls (impl Trait for Type) satisfy a shared contract and enable generic/dynamic dispatch
- • "Why return Self from constructors?" — Self aliases the implementing type, so renaming the struct doesn't break the signature; also mention multiple impl blocks for organization, bounded generics, and cfg-gating
Cheat Sheet
impl Blocks and Methods
Methods (receiver in the first parameter):
impl Counter {
fn new() -> Self { Counter { count: 0 } } // associated fn: Counter::new()
fn increment(&mut self) { self.count += 1; } // mutable borrow
fn value(&self) -> u32 { self.count } // shared borrow
fn reset(self) -> Self { /* consumes */ }
}
Receivers:
| Form | Allows | Error if misused |
|---|---|---|
&self |
read fields; many concurrent borrows | — |
&mut self |
write fields; one live borrow | E0502 when a &T is also held |
self |
consume / move the value | caller's binding unusable afterward (E0382) |
Auto-ref: obj.m() → Type::m(&obj) or Type::m(&mut obj). Why dot vs :: matters: methods use dots, associated functions use Type::name().
Self: alias for the implementing type — -> Self, Self { .. }. Rename-proof.
Multiple impl blocks: organization · impl<T> / impl<T: Trait> generics · #[cfg(...)] gating. Rule: no duplicate inherent method names — E0592.
Preview — trait impl (next topic): impl Trait for Type { } — shared contract; inherent methods always resolve first.