Trait objects vs generics in Rust: dyn Trait vs impl Trait
When to use generics (impl Trait, static dispatch) and when to use trait objects (dyn Trait, Box<dyn Trait>, dynamic dispatch) in Rust.
Rust gives you two ways to write code that works with "any type implementing Shape":
fn print_area(s: &impl Shape) // generics: static dispatch
fn print_area(s: &dyn Shape) // trait object: dynamic dispatch
Both compile and print the same thing. The difference is when the compiler decides which area method to call.
Generics: static dispatch
With impl Shape or <T: Shape>, the compiler generates a separate copy of the function for each concrete type (monomorphization). Calls are direct and can be inlined, so it's as fast as hand-written code. But each use has one concrete type: a Vec<T> holds only circles, or only squares.
Trait objects: dynamic dispatch
dyn Shape means "some type implementing Shape, decided at run time". A trait object is always behind a pointer (&dyn Shape, Box<dyn Shape>, Arc<dyn Shape>), stored as two pointers: one to the data, one to a vtable, a table of that type's method addresses. Each call looks up the method in the table, a small cost that prevents inlining.
What you get in return: different types in one collection.
trait Shape {
fn area(&self) -> f64;
}
struct Circle(f64);
struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 { 3.14 * self.0 * self.0 }
}
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
}
fn main() {
let shapes = vec![Circle(1.0), Square(2.0)];
println!("{}", shapes.len());
}
error[E0308]: mismatched types
A Vec holds one type, so Circle and Square can't share it. Vec<Box<dyn Shape>> can: vec![Box::new(Circle(1.0)), Box::new(Square(2.0))].
The same applies to return types: fn make(round: bool) -> impl Shape must return one concrete type on every path (otherwise: "if and else have incompatible types"). Return Box<dyn Shape> to choose at run time.
Choosing
Generics / impl Trait |
dyn Trait |
|
|---|---|---|
| Dispatch | compile time, inlinable | run time, via vtable |
| Mixed types in one collection | no | yes |
| Binary size | grows per type used | one copy |
| Trait requirements | any trait | must be dyn compatible |
| Typical use | utility functions, iterators, closures, hot loops | plugin lists, UI widgets, handlers, Box<dyn Error> |
Default to generics. Reach for dyn when you need a heterogeneous collection, want to choose an implementation at run time (from config), or want to keep compile times and binary size down for rarely called code.
The third option: an enum
If you know every type up front (Circle, Square, Triangle), an enum Shape { Circle(f64), Square(f64) } with match is often the simplest and fastest choice: no boxing, no vtable, and the compiler checks every case. Trait objects win when other code (or other crates) must be able to add new types.
dyn compatibility
Not every trait can be a trait object. A trait whose methods are generic, return Self, or that requires Sized can't work through a vtable:
fn main() {
let items: Vec<Box<dyn Clone>> = Vec::new();
println!("{}", items.len());
}
error[E0038]: the trait `Clone` is not dyn compatible
(Older compilers call this "object safety".) Add where Self: Sized to a problematic method to exclude it from the vtable, or split the trait.
Example
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle(f64);
struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 {
3.14159 * self.0 * self.0
}
fn name(&self) -> String {
format!("circle r={}", self.0)
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
fn name(&self) -> String {
format!("square side={}", self.0)
}
}
// Static dispatch: one copy per concrete type.
fn describe(s: &impl Shape) -> String {
format!("{} has area {:.2}", s.name(), s.area())
}
// Dynamic dispatch: one copy, decides at run time.
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn make(round: bool, size: f64) -> Box<dyn Shape> {
if round { Box::new(Circle(size)) } else { Box::new(Square(size)) }
}
fn main() {
println!("{}", describe(&Circle(1.0)));
println!("{}", describe(&Square(2.0)));
let shapes: Vec<Box<dyn Shape>> = vec![make(true, 1.0), make(false, 2.0), make(false, 0.5)];
for s in &shapes {
println!("- {}", s.name());
}
println!("total area {:.2}", total_area(&shapes));
}
Output:
circle r=1 has area 3.14
square side=2 has area 4.00
- circle r=1
- square side=2
- square side=0.5
total area 7.39