Reading — step 1 of 7
Learn
Generics give compile-time polymorphism (monomorphization — one specialized version per type). Trait objects give RUNTIME polymorphism via dynamic dispatch — like virtual functions in C++ or interface references in Java.
The Rust Book Ch 17 contrasts the two approaches.
Static vs dynamic dispatch
Static (generic):
fn announce<T: Display>(item: T) {
println!("{}", item);
}
The compiler creates a separate copy of announce for each concrete type used. Direct calls, fully optimized.
Dynamic (trait object):
fn announce(item: &dyn Display) {
println!("{}", item);
}
One announce exists. &dyn Display is a fat pointer: data pointer + vtable pointer. Each method call goes through the vtable.
Box<dyn Trait> for owned heterogeneous collections
Generics can't hold mixed types in a Vec — Vec<T> requires all elements to be the same T. Trait objects can:
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius.powi(2) } }
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } }
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes {
println!("{}", s.area());
}
}
A Vec<Circle> can only hold Circles. A Vec<Box<dyn Shape>> can hold ANY type implementing Shape.
Object safety
Not all traits can be made into trait objects. The trait must be object-safe:
- All methods take
&self,&mut self, orBox<Self>(noselfby value) - No methods return
Self - No generic methods
- No
Sizedsuper-trait
trait Bad {
fn clone(&self) -> Self; // returns Self — not object-safe
fn process<T>(&self, x: T); // generic — not object-safe
}
// Box<dyn Bad> — compile error: not object-safe
Object-safe traits work as dyn Trait; non-object-safe traits can only be used with generics.
When to use which
Generics (T: Trait):
- Performance matters — direct calls inline
- Heterogeneous types not needed
- Code size acceptable (one copy per type)
Trait objects (dyn Trait):
- Need to store mixed concrete types in one collection
- Need to choose implementation at runtime (plugin systems)
- Performance penalty acceptable (vtable indirection)
- Want smaller binaries (one definition, not N specializations)
Most Rust code uses generics. Trait objects appear in real code mainly for plugin/strategy patterns.
impl Trait — between the two
impl Trait in return position is generic-like with extra ergonomics:
fn make_iter() -> impl Iterator<Item = i32> {
(1..10).filter(|x| x % 2 == 0)
}
The caller sees "some iterator over i32" — the concrete type is hidden but fixed. No vtable cost. Different from Box<dyn Iterator<Item = i32>> which has dynamic dispatch.
Common mistakes
- Reaching for trait objects when generics work — pays the vtable cost for no benefit.
- Confusing dyn Trait with impl Trait — impl is static-typed (one type), dyn is dynamic (any type implementing the trait).
- Forgetting Box for owned trait objects —
Vec<dyn Shape>is unsized; needsVec<Box<dyn Shape>>. - Trying to use generic methods through dyn — not object-safe. Either drop the generic or move the method out of the trait.
- Cloning dyn Trait — Clone returns Self, which isn't object-safe. Use a workaround like dyn-clone crate or DynClone.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…