You have now written a Deref impl, shadowed a Vec method by accident, and
met E0034. All three came from the same place: the algorithm the compiler runs
when it sees x.foo(). It is small enough to memorise and it explains a
disproportionate share of Rust’s confusing errors, so it is worth learning
once, deliberately, rather than absorbing by injury.
The algorithm
Given a receiver expression x of type T and a method name foo:
Step 1 — build the candidate type list. Start with T. Repeatedly apply
Deref and append each target, until the chain ends. Then append the
unsized-coercion result if there is one ([T; N] gains [T]).
For x: Rc<RefCell<i32>> the list is:
Rc<RefCell<i32>> → RefCell<i32> → i32
Step 2 — at each type in that list, in order, try three receiver forms:
-
by value —
U -
by shared reference —
&U(this is autoref) -
by mutable reference —
&mut U
Step 3 — take the first match and stop. At each of those nine-ish probes, inherent methods are considered before trait methods. If exactly one candidate matches, it wins. If two or more match at the same step, that is E0034.
Three consequences fall straight out, and they are the ones that bite:
-
Inherent methods always beat trait methods at the same depth. This is why
a
Vecyou have wrapped keeps giving you yourlen, silently. - Shallower beats deeper. A method on the wrapper always beats the same name on the target, no matter how “obviously” the target’s is the one you meant.
-
&is tried before&mut. A&selfmethod shadows a&mut selfmethod of the same name one step later.
💡let v = vec![1, 2, 3]; then v.len(). Vec::len takes &self, but v is a value, not a reference. Why does this compile without you writing (&v).len()?
click to reveal
Because of autoref, step 2.2. The compiler tries Vec<i32> by value first
— there is no fn len(self) — then tries &Vec<i32>, finds
Vec::len(&self), and inserts the & for you.
This is such a common accommodation that most people never notice it exists.
You notice when it cannot help: if the method needs &mut self and your
binding is not mut, the autoref to &mut fails and you get E0596, “cannot
borrow as mutable”. The error mentions mutability rather than method
resolution, which hides where the failure really happened.
The same machinery is why "hello".len() works: &'static str is already a
reference, so the by-value probe on str matches directly after one deref
step.
The .borrow() trap, worked through
This is the canonical misfire, and it is worth doing in full because every piece of the algorithm shows up in it.
RefCell<T> has an inherent method fn borrow(&self) -> Ref<'_, T> —
the runtime-checked shared borrow that is the whole point of the type. The
std::borrow::Borrow trait also has a method called borrow. Now:
use std::borrow::Borrow; // <- the trait is in scope
use std::cell::RefCell;
use std::rc::Rc;
let x: Rc<RefCell<i32>> = Rc::new(RefCell::new(5));
let b = x.borrow();
Run the algorithm. The candidate list starts at Rc<RefCell<i32>>. Does
anything named borrow match at depth 0? Rc has no inherent borrow — but
the Borrow trait is in scope and Rc implements it. Match at depth 0.
The search stops before it ever reaches RefCell.
On rustc 1.95 that particular line does not even compile, and the error is instructive:
error[E0283]: type annotations needed for `&_`
= note: multiple `impl`s satisfying `Rc<RefCell<i32>>: Borrow<_>` found
in the following crates: `alloc`, `core`:
- impl<T, A> Borrow<T> for Rc<T, A> where A: Allocator, T: ?Sized;
- impl<T> Borrow<T> for T where T: ?Sized;
Two Borrow impls apply — Rc<T>: Borrow<T> and the reflexive
T: Borrow<T> — so the compiler cannot pick a Borrowed type. Note what it
is not confused about: it never considered RefCell::borrow at all.
Pin the type and it compiles, silently doing the wrong thing:
let b: &RefCell<i32> = x.borrow(); // Borrow::borrow — NOT what you wanted
You asked for the cell’s contents and got a reference to the cell.
💡You have x: Rc<RefCell<i32>> and use std::borrow::Borrow; somewhere in the module. Write three different expressions that give you the Ref<'_, i32> you actually wanted, and say which one you would put in a code review.
click to reveal
All three work:
let a = RefCell::borrow(&x); // name the type, let the arg coerce
let b = (*x).borrow(); // deref past Rc first, then resolve
let c = <RefCell<i32>>::borrow(&x); // fully qualified
RefCell::borrow(&x) is the one to write. It is short, it names exactly which
borrow you mean, and the &x coerces through Rc‘s Deref at the argument
position. (*x).borrow() also works but reads like a mistake — a future
reader will “clean it up” to x.borrow() and reintroduce the bug. The fully
qualified form is for when even the type is ambiguous.
A fourth option is better than all of them where you control the imports:
do not use std::borrow::Borrow. A trait only participates in method
resolution when it is in scope, so removing the import removes the collision
entirely. This is a genuine argument against blanket use foo::*; — which is
also why clippy::wildcard_imports exists.
Why this is the real argument against Deref polymorphism
The previous problem said “std recommends implementing Deref only for smart
pointers”. Now you can state the reason precisely:
Implementing Deref splices your type into the front of the target’s
resolution chain. Every method the target has, and every method it will ever
gain, is now reachable through your type — and every method you define is a
potential silent shadow of one of theirs, resolved in your favour, with no
diagnostic.
When the collision is between an inherent method and a trait method, or between two depths, the compiler picks and says nothing. It only stops and asks when two candidates tie at exactly the same step. So the loud failure (E0034) is the lucky case; the quiet one is the dangerous case.
The lints that live here
These are the clippy lints that exist because of this algorithm. Knowing which ones actually gate you matters:
| lint | what it catches | gates you? |
|---|---|---|
explicit_auto_deref |
&*x where &x suffices |
yes, default-on |
needless_borrow |
&x immediately dereferenced again |
yes, default-on |
should_implement_trait |
inherent fn add/fn next/fn from_str… on a public type, shadowing a std trait method |
yes, default-on |
wrong_self_convention |
fn to_x(self) or fn is_x(&mut self) — receiver does not match the name’s convention |
yes, default-on |
borrowed_box |
&Box<T> in a signature where &T would do |
yes, default-on |
explicit_deref_methods |
calling .deref() by name instead of using * |
no — nursery, allow by default |
should_implement_trait is the direct descendant of everything above: if you
name an inherent method add, then x.add(y) resolves to yours and never to
std::ops::Add::add, and every reader will guess wrong. Clippy’s message is
literally “defining a method called add on this type; consider implementing
the std::ops::Add trait or choosing a less ambiguous name”.
The short version, worth keeping
- Build the deref chain from the receiver’s type.
-
At each step try by value, then
&, then&mut. - First match wins; inherent beats trait; ties are E0034.
- A trait must be in scope to participate at all.
-
If two things could plausibly be meant, name one explicitly —
Type::method(&x)or<Type as Trait>::method(&x).
Rule 4 is the one people forget in both directions: it is why an extension
trait needs a use, and it is why an unnecessary use can quietly change
what your code does.