Skip to content
← All articles

How x.foo() actually resolves

Every confusing 'method not found' and every E0034 in this course comes from one small algorithm. It is invisible until it misfires — so here it is, written out, with the classic `.borrow()` trap worked through.

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:

  1. by value — U
  2. by shared reference — &U (this is autoref)
  3. 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 Vec you have wrapped keeps giving you your len, 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 &self method shadows a &mut self method 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

  1. Build the deref chain from the receiver’s type.
  2. At each step try by value, then &, then &mut.
  3. First match wins; inherent beats trait; ties are E0034.
  4. A trait must be in scope to participate at all.
  5. 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.