Trait Objects and Dynamic Dispatch
So far traits have been used as bounds on generics (T: Trait). That is static
dispatch: the compiler knows the concrete type and picks the method at compile time.
Sometimes you instead want to treat many different types uniformly at runtime ā that
is what trait objects (dyn Trait) are for. This is the everyday polymorphism a C#
developer already knows as an interface reference.
Throughout this file we use one trait and two types:
trait Shape {
fn area(&self) -> f64;
}
struct Circle { r: f64 }
struct Rectangle { w: f64, h: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.w * self.h }
}Static dispatch: generics (T: Trait / impl Trait)
fn print_area<T: Shape>(s: &T) {
println!("{}", s.area());
}The compiler monomorphizes this: it stamps out a separate, specialized copy of
print_area for each concrete T you call it with. Calls can be inlined, so it's fast ā
at the cost of a larger binary.
In C#: like void PrintArea<T>(T s) where T : IShape. The mechanics differ: .NET keeps
generics reified at runtime, sharing one JIT-compiled body across all reference-type
arguments but specializing a separate copy for each value type (List<int> vs
List<double>). So value-type generics already behave much like Rust's monomorphization;
the differences are that .NET shares code across reference types and specializes at runtime
(JIT), whereas Rust stamps out a copy per type at compile time.
Dynamic dispatch: trait objects (dyn Trait)
fn print_area(s: &dyn Shape) {
println!("{}", s.area());
}There is now one print_area. The concrete type is erased to "some Shape", and
which area to call is looked up at runtime through a vtable.
In C#: this is exactly an interface reference ā void PrintArea(IShape s) ā a virtual
call through a method table. In C# that's the default; in Rust you opt into it with dyn.
When you NEED a trait object: heterogeneous collections
A Vec<T> holds one concrete type. To store different types together, use a trait
object behind a pointer ā usually Vec<Box<dyn Shape>>:
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { r: 1.0 }),
Box::new(Rectangle { w: 2.0, h: 3.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();In C#: List<IShape> with mixed Circle/Rectangle ā the same idea. Rust just makes
the heap allocation (Box) and the dynamic dispatch (dyn) visible in the type.
&dyn vs Box<dyn>
&dyn Traitā a borrowed trait object: no ownership, no allocation. Great for function parameters (fn print_area(s: &dyn Shape)).Box<dyn Trait>ā an owned, heap-allocated trait object. Use it to store, return, or collect trait objects (Vec<Box<dyn Shape>>,-> Box<dyn Shape>).
In C#: both are "an interface reference"; Rust distinguishes borrowed vs owned because ownership is always explicit.
What a trait object really is: a fat pointer
A &dyn Trait / Box<dyn Trait> is two pointers wide ā one to the data, one to a
vtable (the concrete type's method addresses, plus its size and destructor):
&dyn Shape
āāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā
ā data ptr ā vtable ptr ā
āāāāāāāā¬āāāāāāāā“āāāāāāā¬āāāāāāāā
ā ā
ā¼ ā¼
the Circle āāāāāāāāāāāāāāāāāāāā
{ r: 1.0 } ā area: fn(*self)⦠ā ā Circle's Shape methods
ā drop, size, align ā
āāāāāāāāāāāāāāāāāāāās.area() reads the function pointer out of the vtable and calls it.
In C#: this vtable is the same machinery behind interface/virtual dispatch ā the
difference is that Rust puts "this value is reached through a vtable" into the type, as
dyn. (A plain &[T]/&str is also a fat pointer, but its second word is a length, not
a vtable ā see module 07.)
This is what Box<dyn Error> means
You have seen Result<_, Box<dyn Error>> in modules 10/19/25/26/31. It now reads plainly:
a heap-allocated trait object holding any type that implements std::error::Error. The
? operator converts each concrete error into that trait object via From (see
02_conversions.md). It's the "any error" catch-all ā conceptually like catching
Exception in C#, but opt-in and written in the type.
Object safety (a.k.a. "dyn compatibility"): why some traits can't be dyn
A trait can become a trait object only if every method is callable through a vtable.
The main rules: methods take a self receiver (&self/&mut self), are not generic,
and don't return Self. A trait with fn new() -> Self or fn parse<T>(...) is not
object-safe ā there'd be no single function to store in the vtable, and the compiler
says so (recent Rust calls this property dyn compatibility):
error[E0038]: the trait `Build` is not dyn compatibleIn C#: nearly any interface can be used as a reference, so this is a Rust-specific
check. The fix is usually to keep the object-unsafe methods on a generic API and expose a
dyn-friendly subset, or take &self.
Which one should I use?
Static (T: Trait / impl Trait) | Dynamic (dyn Trait) | |
|---|---|---|
| Dispatch | Compile-time, can inline | Runtime vtable lookup |
| Binary size | Larger (a copy per type) | Smaller (one copy) |
| Types | Fixed at compile time | Can be mixed at runtime |
| Reach for it when | Hot paths; one type per call site | Heterogeneous collections, plugin-style APIs, smaller code |
In C#: C# generics are the rough analog of static dispatch (though reified), and
interface references are dynamic dispatch. Rust makes the choice explicit: default to
generics, and reach for dyn when you need to mix types or shrink code size.
Key takeaway
dyn Trait is a trait object: dynamic dispatch through a vtable, represented as a fat
pointer (data + vtable). It is the explicit form of C#'s interface-reference polymorphism.
Use &dyn/Box<dyn> for heterogeneous collections (Vec<Box<dyn Trait>>) and "any error"
(Box<dyn Error>); use generics (T: Trait) otherwise. Only object-safe traits can be
made into trait objects.