Rust Traits vs Interfaces
Rust traits look like interfaces from other languages, but the differences matter: associated types, blanket impls, coherence, and dynamic dispatch all behave their own way.
What you'll learn
- ✓How Rust traits differ from Java/C# interfaces
- ✓Static vs dynamic dispatch
- ✓Associated types and where they shine
- ✓Blanket impls and the orphan rule
- ✓Common patterns and pitfalls
Prerequisites
- •Basic familiarity with the language
If you arrived at Rust from Java, C#, or TypeScript, traits will feel like interfaces. Same idea: declare a set of methods, implement them for your types, use the abstraction in generic code. The syntax even rhymes. But traits have features and constraints that interfaces do not, and the differences shape how you design Rust APIs.
What a trait is
A trait is a collection of method signatures and (optionally) default implementations. Any type can implement a trait by providing the required methods.
trait Speak {
fn speak(&self) -> String;
fn shout(&self) -> String {
self.speak().to_uppercase()
}
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String {
"woof".into()
}
}
let d = Dog;
println!("{}", d.shout()); // WOOF
Default methods can call required methods. That alone is more than many interface systems offer.
Static dispatch by default
When you use a trait as a generic bound, Rust monomorphizes: it generates a separate copy of the function for each concrete type at compile time. There is no virtual call, no vtable, and no runtime overhead.
fn announce<T: Speak>(thing: &T) {
println!("{}", thing.speak());
}
Calling announce(&dog) generates a version of announce specialized for Dog. The call is direct.
Dynamic dispatch with trait objects
If you want a heterogeneous collection or a runtime-chosen implementation, use a trait object. The type is dyn Trait, behind a pointer.
let speakers: Vec<Box<dyn Speak>> = vec![Box::new(Dog), Box::new(Cat)];
for s in &speakers {
println!("{}", s.speak());
}
dyn Speak carries a pointer to the data and a pointer to a vtable, exactly like a Java interface. The cost is one indirection per call. Use trait objects when you need runtime polymorphism or want to keep binary size down.
The mental model
Static (T: Speak): fn call_speak(x: &Dog) { x.speak_for_Dog() }
fn call_speak(x: &Cat) { x.speak_for_Cat() }
monomorphized at compile time
Dynamic (&dyn Speak): fn call_speak(x: &dyn Speak)
|
v
[data ptr][vtable ptr]
|
v
{ speak_for_T } The compiler picks based on whether the abstraction needs to exist at runtime. Generics give you maximum speed; trait objects give you flexibility.
Associated types
A trait can declare type members, not just methods. These are filled in by each implementor.
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.count += 1;
if self.count <= 5 { Some(self.count) } else { None }
}
}
This is closer to Java’s interface Iterator<T> but with a key difference: Counter can implement Iterator only one way (one Item type). With generic parameters, Counter could implement Iterator<u32> and Iterator<String> simultaneously, which is sometimes useful and sometimes a footgun. Associated types prevent that ambiguity.
Blanket impls
You can implement a trait for every type that satisfies some bound. The standard library does this everywhere.
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
Now every type that implements Display also gets to_string for free. Blanket impls are how Rust composes abstractions. No interface system in mainstream OO languages offers anything quite like them.
The orphan rule
You can implement your trait for any type, and any trait for your type, but not someone else’s trait for someone else’s type. This is the coherence rule, sometimes called the orphan rule.
impl MyTrait for Vec<i32> { /* fine: my trait, foreign type */ }
impl Display for MyStruct { /* fine: foreign trait, my type */ }
// impl Display for Vec<i32> { /* not allowed: both foreign */ }
This guarantees that there is only one implementation of any (trait, type) pair anywhere in the dependency graph. Java and C# do not need this because they do not allow third parties to add interface implementations after the fact. The workaround in Rust is the newtype pattern: wrap the foreign type in a tuple struct you own.
Common pitfalls
Confusing generics with trait objects. fn foo<T: Trait>(x: T) and fn foo(x: &dyn Trait) look similar but compile and dispatch very differently.
Trying to mix trait objects of different traits. There is no inheritance: Box<dyn A> is not assignable to Box<dyn B> unless A: B (super-trait relationship).
Making traits with non-object-safe methods. Methods returning Self or with generic parameters cannot be used through dyn Trait. The compiler will tell you, but redesigning is sometimes painful.
Reaching for Box<dyn Trait> too early. Generics with bounds usually compile to faster, easier-to-debug code. Use trait objects only when you actually need runtime polymorphism.
Practical tips
Start with generics. Switch to trait objects when binary size or runtime selection demands it. Use associated types when the type is determined by the implementor; use generic parameters when the caller chooses.
Lean on blanket impls. They are the most idiomatic way to extend functionality across many types at once. Just be aware they can cause coherence conflicts if you also write specific impls that overlap.
When you need to extend a foreign type, use the newtype pattern. Wrapping a Vec<T> in MyVec(Vec<T>) and implementing on MyVec is the standard escape hatch.
Wrap-up
Traits are interfaces with sharper edges and more flexibility. The big differences are blanket impls, associated types, the orphan rule, and the static/dynamic dispatch choice. Once you internalize that traits are a tool for composition, not just an interface system, Rust APIs start to make sense and the standard library reveals patterns you can copy in your own code.
Related articles
- Rust Rust Deref and AsRef Explained
Demystify Rust's Deref, DerefMut, and AsRef traits with clear examples showing how smart pointers, coercions, and flexible APIs really work.
- Rust Rust Iterators and Adapters
Master Rust's iterator trait, lazy adapters like map and filter, and consumers like collect and fold for fast, expressive data processing.
- Rust Rust Traits: Shared Behavior Without Inheritance
Understand how Rust traits define shared behavior, with default methods, trait bounds, derive, and a clear take on dyn Trait versus impl Trait.
- Rust Rust Actix vs Axum Comparison
A side-by-side comparison of Actix Web and Axum, covering architecture, ergonomics, performance, ecosystem, and how to pick the right one for your project.