Plugin systems, heterogeneous collections and error boxes need dynamic dispatch rather than generics.
Generics pick the concrete type at compile time. A trait object, dyn Trait, picks it at runtime. Because the size of the underlying type is unknown, a trait object always sits behind a pointer: &dyn Draw, Box<dyn Draw>, Rc<dyn Draw> or Arc<dyn Draw + Send + Sync>.
Such a pointer is a fat pointer: two words, one pointing to the data and one to a vtable, a static table holding the concrete type's method pointers plus its size, alignment and drop function. Calling a method is an indirect call through the vtable. That costs a little (mostly by blocking inlining), but you get one compiled copy of the calling code and can mix types in one collection: Vec<Box<dyn Draw>>.
Not every trait can be a trait object. A trait is dyn compatible (formerly "object safe") only if its methods could be called through a vtable, which rules out:
- generic methods (
fn visit<T>(&self, t: T)), since the vtable cannot hold infinitely many instantiations, - methods that mention
Selfoutside the receiver, such as returningSelf, - associated constants, and static functions without
self.
You can keep such methods on an otherwise compatible trait by adding where Self: Sized, which excludes them from the vtable. Clone is not dyn compatible, so cloning boxed objects needs a helper method like fn box_clone(&self) -> Box<dyn Shape>.
Since Rust 1.86, trait upcasting works: if trait Shape: Debug, a &dyn Shape coerces to &dyn Debug. To get back the concrete type, use std::any::Any and downcast_ref::<T>().
Rule of thumb: generics by default; dyn when you need a heterogeneous collection, want to reduce compile time or binary size, or need to choose implementations at runtime.
Example
use std::fmt::Debug;
trait Shape: Debug {
fn area(&self) -> f64;
fn name(&self) -> String { "shape".into() }
fn scaled(&self, k: f64) -> Self where Self: Sized; // excluded from the vtable
}
#[derive(Debug)] struct Circle(f64);
#[derive(Debug)] struct Square(f64);
impl Shape for Circle {
fn area(&self) -> f64 { 3.14159 * self.0 * self.0 }
fn scaled(&self, k: f64) -> Self { Circle(self.0 * k) }
}
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
fn scaled(&self, k: f64) -> Self { Square(self.0 * k) }
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Square(2.0).scaled(2.0))];
println!("{:.2}", shapes.iter().map(|s| s.area()).sum::<f64>()); // 19.14
let as_debug: &dyn Debug = shapes[1].as_ref(); // trait upcasting (1.86+)
println!("{as_debug:?} {}", shapes[0].name()); // Square(4.0) shape
println!("{}", size_of::<&dyn Shape>() == 2 * size_of::<usize>()); // true: fat pointer
}Remember
dyn Traitlives behind a pointer that carries a vtable: one copy of code, indirect calls- Generic methods and
Self-returning methods break dyn compatibility unlesswhere Self: Sized - Use
dynfor heterogeneous collections and runtime choice; generics otherwise
Watch out for
- Defaulting to
Box<dyn Trait>everywhere out of habit from OOP languages, where generics would be simpler and faster.