We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 18 of 20
Method receivers and the consuming builder
Write a builder that eats itself, and a companion method that does not.
pub fn build_configs(specs: Vec<Vec<String>>) -> Vec<String>
Reading a receiver
A method’s first parameter tells you, at a glance, what the call does to your value. There are only four shapes and they are worth being able to read instantly:
| Receiver | Sugar for | After the call |
|---|---|---|
fn f(&self) |
self: &Self |
value untouched; you may call it again |
fn f(&mut self) |
self: &mut Self |
value untouched but possibly changed |
fn f(self) |
self: Self |
value moved into the method; gone |
fn f(mut self) |
self: Self (mutable binding) |
same as above — still consuming |
That last row is the one that confuses people. mut self is not a fourth
kind of borrow. It is a by-value receiver whose local binding happens to be
mutable, exactly like fn f(mut x: String). The mut belongs to the
pattern, not to the type. The caller cannot tell the difference and does not
care.
This distinction is a daily skill. s.len() takes &self, so s survives.
s.into_bytes() takes self, so it does not. Reading a signature and knowing
which one you are looking at, without checking the docs, is most of what fluency
in Rust feels like.
The consuming builder
Config::new()
.retries(3)
.tag(String::from("fast"))
.build()
Each step takes self, modifies it, and returns Self. The value flows through
the chain, owned by exactly one frame at a time, and build swallows it for
good. No clones, no borrows, no lifetimes, and — because each step owns the
whole config — no way for a half-configured Config to be used by accident.
This is the first design pattern in the course where ownership is a feature
rather than an obstacle. The type system is enforcing “you may configure this,
then build it once” with no runtime checks and no Option fields.
::: question What breaks if a builder step takes &mut self and returns &mut Self instead?
It works, and it is a real alternative — but it changes what callers can do.
A &mut self builder must be anchored in a variable, because you cannot chain
off a temporary and then return the result:
let mut c = Config::new();
c.retries(3).tag(t); // fine
let s = Config::new().retries(3).build(); // borrow of a temporary; awkward
and build(self) cannot be reached from a &mut Self chain at all — you would
have to make it build(&self) -> String and clone the internals, or make it
build(&mut self) and leave the config in a mysterious post-build state.
The consuming form has the opposite trade-off: it composes beautifully in an
expression, and it cannot be used to configure a value you want to keep. Most
of the standard library picks consuming (Iterator adapters are all self);
most GUI and HTTP-client crates pick it too. When you want both, the usual trick
is a &mut self core with thin self-taking wrappers.
:::
What to build
Write struct Config { retries: u32, tags: Vec<String> } and:
-
Config::new()— retries0, no tags -
fn retries(mut self, n: u32) -> Self -
fn tag(mut self, t: String) -> Self -
fn to_summary(&self) -> String—format!("r{} t{}", retries, tags.len()) -
fn build(self) -> String—format!("retries={};tags={}", retries, tags.join(","))
Then build_configs runs one spec per config. Each spec is a list of commands:
-
"retries N"— apply.retries(N); a value that does not parse counts as0 -
"tag T"— apply.tag(T) -
"summary"— recordto_summary(); the config must still be usable afterwards - anything else — ignore
The output element for a spec is format!("[{}] {}", notes.join("|"), built),
where notes are the recorded summaries in order. So
["retries 3", "tag a", "summary", "tag b"] gives
"[r3 t1] retries=3;tags=a,b", and an empty spec gives "[] retries=0;tags=".
The chain must work without a single .clone(). If you find yourself
needing one, a receiver somewhere is the wrong shape.
Two things the gate will tell you
The builder step’s return value is not optional. The starter writes
cfg.retries(n.parse().unwrap_or(0));
as a statement, discarding the result. That does not “modify cfg in place” —
it moves cfg into the method and drops whatever came back, and the next line
that touches cfg is E0382.
to_summary may not take self. If it does, clippy stops you before the
tests do:
error: methods with the following characteristics: (`to_*` and `self` type is
not `Copy`) usually take `self` by reference
wrong_self_convention is on by default and encodes the standard library’s
naming convention:
| Prefix | Receiver | Meaning |
|---|---|---|
as_ |
&self / &mut self |
free, borrowed view (as_str, as_slice) |
to_ |
&self |
expensive, produces owned data (to_string, to_vec) |
into_ |
self |
consumes, usually cheap (into_bytes, into_iter) |
is_ / has_ |
&self |
a question, not a transformation |
Free, enforced API design advice. The exception the lint knows about is Copy
types, where to_ on self costs nothing and is allowed.
One more, from rustc rather than clippy: if you write mut self on a method
that never mutates, unused_mut fires and -D warnings fails you. The mut is
a claim; make it true.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.