RFA-530 · Case file with fixtures · Case 502 of 694 · Compiler evidence
Rust Method Receivers Must Lead Back to Self
The first self parameter is not ordinary dependency injection. Its type must identify the type whose method namespace contains the function.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The self parameter carries method ownership and lookup identity, so it cannot be replaced by an unrelated dependency type.
- First discriminating check
- Identify the type owning the impl, choose a supported receiver rooted in Self, and pass unrelated collaborators as ordinary parameters.
I once read self: SomeType as if self were only a conveniently named first argument. E0307 is the useful correction: a method receiver has a structural relationship with the type owning the method.
The failing fixture opens impl Cache but declares fn refresh(self: &Scheduler). Scheduler may be a perfectly sensible dependency. It is not the receiver of a method stored in the Cache namespace.
A receiver answers which value owns this call
The familiar forms are self, &self, and &mut self. They expand conceptually to self: Self, self: &Self, and self: &mut Self. Inside impl Cache, Self means Cache.
That connection enables cache.refresh(). Method-call lookup begins from the type of cache, follows the permitted receiver adjustments, and finds associated methods whose receiver can be reached. A completely unrelated &Scheduler breaks this model.
The official E0307 explanation lists the stable receiver families and explains the role of Self.
The repaired method borrows the actual owner
The repaired fixture gives Cache a generation field and uses fn refresh(&self). The call now reads state from the receiver that selected the method.
If refreshing also needs a scheduler, I pass it as a second parameter:
fn refresh(&self, scheduler: &Scheduler) -> u64
This signature tells the truth. The cache owns the operation; the scheduler collaborates with it. Swapping those roles merely to satisfy syntax would produce a confusing API.
Box, Rc, Arc, and Pin receivers still preserve identity
Rust accepts some explicit forms such as self: Box<Self>, self: Rc<Self>, self: Arc<Self>, and supported Pin wrappers. These forms change how the receiver is owned or accessed, but the chain still leads to Self.
For example, a consuming operation may use self: Box<Self> when the value is already heap-owned. An async or self-referential abstraction may expose a pinned receiver. Those are ownership choices around the same underlying method owner, not arbitrary first-argument types.
The Reference describes receivers in its method section. I check that grammar before designing exotic receiver syntax.
Deref affects method lookup but is not invisible inheritance
Method calls may use dereference adjustments. That is why wrapper types can often call methods of their target. The Deref documentation also warns that this has API-design consequences because methods become part of lookup.
I do not implement Deref only to force a desired method receiver. I use it when transparent pointer-like behaviour is genuinely the abstraction. A service container or scheduler is usually composition, so an ordinary parameter or field is clearer.
Arbitrary self types extend receiver possibilities on nightly, but this does not turn self into an unrestricted argument. The receiver still has to follow an accepted chain toward Self. I keep stable public APIs on stable rules unless the project deliberately owns a nightly toolchain.
An associated function is sometimes the honest design
If an operation does not need a Cache value, I omit self entirely:
impl Cache {
fn from_scheduler(scheduler: &Scheduler) -> Cache {
// construct a cache
}
}
This is an associated function and callers use Cache::from_scheduler(...). The path says that Cache organises the constructor, while the scheduler is ordinary input. Trying to disguise this as scheduler.from_cache_namespace() would blur ownership.
Trait receiver mismatch is a related but different failure
There is another easy mistake: a trait declares fn refresh(&self), while its implementation declares a different valid receiver form or parameter type. That may produce a trait compatibility diagnostic such as E0053. E0307 is more basic: the written receiver type itself is not an accepted receiver for the surrounding Self.
I preserve this distinction when searching compiler errors. Similar-looking source can fail during different checks, and the repair should follow the check that actually rejected it.
My receiver checklist
- Which type's
implblock contains the method? - Does the receiver resolve to that
Selfthrough a supported form? - Should the operation borrow, mutably borrow, or consume its owner?
- Is an unrelated service really a second argument or stored dependency?
- Does the operation need a receiver at all?
- Am I using
Derefbecause the wrapper is pointer-like, or only for syntax? - Is a nightly receiver feature an intentional compatibility cost?
- Does the call syntax communicate who owns the behaviour?
The core principle is that self carries method identity, not only data. Once I decide which value owns the operation, the correct receiver and the place for collaborators become much easier to see.