We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Generics and Traits step 13 of 24
Deref polymorphism, and the day your method disappears
Programmers arriving from an object-oriented language discover Deref, notice
that it makes a wrapper “inherit” every method of the wrapped type, and build
a class hierarchy out of it. It compiles. It feels wonderful for about a week.
Then somebody adds a method and nobody can explain what the code does any
more.
This problem is that week, compressed.
Your task
pub struct Inventory { slots: Vec<String> }
impl Deref for Inventory { type Target = Vec<String>; ... }
impl Inventory {
pub fn len(&self) -> usize // counts NON-EMPTY slots
pub fn is_empty(&self) -> bool
}
pub trait Weight { fn total(&self) -> usize; }
pub trait Bulk { fn total(&self) -> usize; }
pub fn counts(slots: Vec<String>) -> Vec<usize>
counts must return four numbers, in this order:
-
the inventory’s own
len— how many slots are non-empty -
the underlying
Vec‘slen— how many slots exist at all -
Weight‘s total — the sum of the slot strings’ byte lengths -
Bulk‘s total — ten times the raw slot count
Everything except the body of counts is already written. All you have to do
is call four methods, and that turns out to be the hard part.
Problem one: your inherent method silently wins
Inventory::len and Vec::len have the same name. Write inv.len() and you
get the inherent one — always. There is no ambiguity error, no warning,
nothing at the call site to hint that a second len exists. Method resolution
builds the deref chain Inventory → Vec<String> → [String] and takes the
first match, and inherent methods always win over anything found deeper.
To reach Vec::len you must deref explicitly first:
(*inv).len() // deref, THEN resolve — Vec::len
Vec::len(&inv) // or name the type and let the argument coerce
Both work. Neither is something you would guess. And notice the direction of
the damage: it is not that your code fails to compile — it is that
inv.len(), which every reader will assume means “how many things are in
here”, means something you invented.
Problem two: two traits, one name
Weight and Bulk both declare fn total(&self). Both are implemented for
Inventory. Write inv.total() and:
error[E0034]: multiple applicable items in scope
|
55 | inv.total(),
| ^^^^^ multiple `total` found
|
note: candidate #1 is defined in an impl of the trait `Bulk` for the type `Inventory`
note: candidate #2 is defined in an impl of the trait `Weight` for the type `Inventory`
help: disambiguate the method for candidate #2
|
55 - inv.total(),
55 + Weight::total(&inv),
E0034 is the honest version of problem one. Here the compiler cannot pick,
so it stops and shows you both candidates. In the len case it could pick, so
it did — quietly. Same underlying algorithm, wildly different experience.
The fix is fully-qualified syntax: Weight::total(&inv). The most explicit
form, which always works, is <Inventory as Weight>::total(&inv); you need
that one when even the trait name is ambiguous.
Why std says don’t do this
The standard library’s position is that Deref is for smart pointers, and
the reason is exactly what you just did: implementing Deref injects your type
into everybody’s method-resolution chain. Every method the target ever gains is
now a method your type appears to have, and every method you add is a potential
silent shadow of one of theirs.
There is no Deref-based way to say “inherit these five methods but not those
three”. It is all or nothing, forever, including methods that do not exist yet.
DerefMut doubles the surface. It is the &mut self half of the same trait,
and it is what lets vec[0] = x work through a wrapper. Everything above
applies to it, plus the autoref rule that a method lookup tries &self before
&mut self at each step, so a &self method on your type shadows a &mut self
method on the target.
The honest counterpoint
Real crates are full of newtype wrappers that deref to the inner type, and
they are fine. The rule is a strong default, not an absolute. What makes the
Inventory above bad is specific and checkable:
- it derefs to a collection, not to a pointee it conceptually is;
- it defines a method that collides with one on the target;
-
the two
lens disagree, so the shadowing changes behaviour rather than just routing it.
Break none of those and a deref newtype is a reasonable design. Break the third one and you have built a trap for your future self.
Finally, keep the three-way distinction straight, because learners collapse it constantly:
-
AsRef<T>— an explicit, cheap “view me as aT“. Called by name. -
Borrow<T>— the same, plus a promise thatEq/Ord/Hashagree. Used byHashMap::get. -
Deref— an implicit conversion that also rewrites method resolution.
Only the third one changes what x.foo() means.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.