A trait defines a set of methods a type can implement, similar in spirit to an interface. Using a trait as a generic bound (fn foo<T: Trait>) resolves at compile time via monomorphization — static dispatch, with zero runtime cost. A trait object, written dyn Trait, instead stores a pointer to the data plus a pointer to a table of its methods (a vtable), resolved at runtime — dynamic dispatch, which allows a single collection to hold several different concrete types, at a small runtime cost per call.
Defining and implementing a trait
A trait can include a default method implementation, which an implementing type can use as-is or override.
trait Shape {
fn area(&self) -> f64;
fn describe(&self) -> String { // default method
format!("This shape has area {:.2}", self.area())
}
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// describe() uses the default — not overridden
}Static dispatch: generics with trait bounds
Using a trait as a generic bound resolves at compile time — the compiler generates a separate, fully specialized version for each concrete type used, exactly like the generics covered earlier.
fn print_area<T: Shape>(shape: &T) { // resolved at compile time
println!("{}", shape.describe());
}Dynamic dispatch: dyn Trait and trait objects
A collection needing to hold several different concrete types behind one interface uses dyn Trait — usually behind a Box, since a trait object's exact size isn't known at compile time.
struct Square { side: f64 }
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
}
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 2.0 }),
Box::new(Square { side: 3.0 }),
];
for shape in &shapes {
println!("{}", shape.describe()); // resolved at RUNTIME via vtable
}