How it works
The explanation, with examples you can run.
Both kinds of dispatch
trait Shape { fn area(&self) -> f64; fn describe(&self) -> String { // default method format!("area {:.1}", self.area()) }} struct Square(f64);struct Circle(f64); impl Shape for Square { fn area(&self) -> f64 { self.0 * self.0 }}impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.0 * self.0 } fn describe(&self) -> String { format!("circle, {:.1}", self.area()) }} // Static dispatch: one copy of this function per concrete T (monomorphisation).fn print_static<T: Shape>(s: &T) { println!("{}", s.describe()); } // Dynamic dispatch: one copy; calls go through a vtable.fn total(shapes: &[Box<dyn Shape>]) -> f64 { shapes.iter().map(|s| s.area()).sum() } fn main() { print_static(&Square(2.0)); print_static(&Circle(1.0)); let mixed: Vec<Box<dyn Shape>> = vec![Box::new(Square(2.0)), Box::new(Circle(1.0))]; println!("{:.2}", total(&mixed)); println!("{} vs {}", size_of::<&Square>(), size_of::<&dyn Shape>()); // thin vs fat pointer}Output: area 4.0, circle, 3.1, 7.14, 8 vs 16.
Static dispatch
fn print_static<T: Shape>(s: &T), fn f(s: &impl Shape) and where T: Shape all mean the same: the compiler monomorphises, producing one specialised copy per type it is called with. Calls are direct and can be inlined, so abstractions cost nothing at run time. The price is compile time and code size, and every element of a Vec<T> must be the same T.
Returning impl Trait (fn evens() -> impl Iterator<Item = u32>) hides the concrete type from the caller while still dispatching statically. In the 2024 edition such a return type captures all lifetimes in scope by default; + use<...> (Rust 1.82, and in traits since 1.87) lists exactly what it captures.
Dynamic dispatch
A &dyn Shape or Box<dyn Shape> is a fat pointer: a pointer to the value and a pointer to the vtable for its concrete type, which is why it is 16 bytes on a 64-bit target. That lets one Vec hold different types, and plugins or handlers be chosen at run time. The cost is an indirect call that usually can't be inlined.
Not every trait can be made into an object. A trait is dyn compatible (formerly "object safe") only if, among other rules, its methods don't return Self and don't have generic type parameters. Since Rust 1.86 a &dyn Sub can be upcast to &dyn Super when Sub: Super.
Generics, bounds and coherence
Generic structs and functions state what they need with bounds: fn largest<T: PartialOrd + Copy>(xs: &[T]) -> T. Common traits such as Debug, Clone, PartialEq and Hash can be derived. Operators are traits too (Add, Index), and so are conversions (From, Into), iteration (Iterator) and clean-up (Drop).
Coherence (the orphan rule) says you can implement a trait for a type only if the trait or the type is defined in your crate. Two crates can never supply conflicting impls, at the cost of the occasional wrapper type (the "newtype" pattern).
Default to generics in libraries for speed; switch to dyn when you need a mixed collection, want to cut compile times, or hide types behind a stable boundary.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.