RFA-421 · Case file with fixtures · Case 393 of 694 · Compiler evidence
Rust Trait Method Parameter Types Must Match Exactly
A trait method signature is the dispatch boundary, so its implementation must match parameter and receiver types. Convert inside the method or change the shared trait contract rather than changing one implementation's callable shape.
- 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
- Generic and dynamic callers compile against one trait signature, so each implementation must preserve its receiver and parameter types and perform representation conversions behind that boundary.
- First discriminating check
- Place the declaration and implementation side by side, expand aliases, and compare receiver, parameter, lifetime, generic, safety, ABI, and return types.
I once changed an implementation to accept the integer type used by its internal decoder. The method name and return type stayed the same, but Rust rejected the implementation with E0053. A trait method is not matched approximately.
The failing fixture declares decode(&self, input: u16) in the trait and uses input: i16 in the impl. Rust points at the parameter and reports “expected u16, found i16.”
Dispatch needs one callable signature
Code generic over a trait compiles against the declaration, not against every implementation body:
fn decode_one<D: Decode>(decoder: &D, value: u16) -> u16 {
decoder.decode(value)
}
The caller is entitled to pass a u16. If a particular implementation accepted only i16, dispatch would need an undocumented conversion. The conversion can lose values above i16::MAX, so the compiler cannot invent it.
The E0053 documentation covers parameter mismatches and receiver mutability mismatches. Both change what a caller must provide.
Convert behind the boundary
The repaired fixture makes the impl accept u16, exactly as declared. If the internal format needs another representation, conversion belongs inside the method:
fn decode(&self, input: u16) -> u16 {
let internal = i16::try_from(input)?;
// decode internal
}
A real fallible conversion also requires a return type able to report failure. That may reveal that the trait should return Result, but it does not justify changing only one implementation's argument type.
I keep wire types, domain types, and storage types separate. The public trait chooses one boundary type; adapters perform explicit conversion at its edges.
Mutability is part of the signature too
Changing &self to &mut self looks smaller than changing an integer type, but it asks callers for exclusive access instead of shared access. Generic code holding only &D could no longer call the implementation.
If decoding needs mutation, I choose among three honest designs:
- declare
&mut selfin the trait for every implementation; - use interior mutability when shared calls genuinely belong to the abstraction;
- keep temporary state local so the receiver remains shared.
Interior mutability is not a free compatibility patch. It introduces runtime borrowing, locking, or atomic rules that should match the concurrency model.
Type aliases can hide the mismatch
Aliases, imported newtypes, and generated code can make E0053 hard to read. I expand the trait declaration and implementation side by side, including lifetimes, mutability, generic parameters, ABI, safety, and return type.
Two types having the same representation does not make them the same type. A type UserId = u16 alias is the same type, while struct UserId(u16) is a distinct type. Newtypes need explicit conversion but can protect domain meaning.
The trait should describe the stable boundary
If every implementation really needs i16, the trait declaration may simply be stale. I change the trait and let compiler errors identify callers and impls that require migration. That coordinated change is safer than leaving disagreement hidden behind casts.
For a public crate, changing a trait method parameter is normally a breaking API change. A new method with a default adapter, a versioned trait, or a newtype transition can provide a migration path.
Generic arguments must also agree
The same rule applies when a parameter contains generics. Vec<u8> is not interchangeable with &[u8], and Option<T> is not interchangeable with T. An implementation can borrow a Vec as a slice internally, but its entry signature still matches the trait.
The Reference on methods treats the receiver and ordinary parameters as part of the function's declaration. Method syntax does not weaken type identity.
I also compile implementations behind optional features. A trait can remain stable while a platform-specific or feature-gated impl quietly falls behind after a signature change. Checking only the default target leaves that mismatch hidden until a user enables the affected backend. The compiler is an effective migration list only when CI builds the combinations the crate promises.
My repair process
When I see E0053, I:
- Copy the trait signature beside the impl.
- Compare the receiver first.
- Expand aliases and check parameter types.
- Check lifetimes, generics, safety, ABI, and return type.
- Decide which type belongs at the abstraction boundary.
- Put explicit conversion inside an adapter or the method body.
- Test values at conversion limits, not only small examples.
The core principle is that dynamic or generic dispatch requires a single stable call shape. Implementations can use completely different algorithms and internal representations, but their doors must have the dimensions published by the trait.