Rust Choosing a Dispatch Style
Dispatch is how a call reaches shared behaviour, through a generic parameter, a trait object, or an enum with a match. Four questions, asked in order, make the choice.
One known type at each call: pick the generic
The caller names the greeter by passing a value. The compiler builds a copy of greet_generic for that type. It may inline the call:
fn greet_generic<G: Greeter>(greeter: &G) -> String {
greeter.greet()
}
println!("generic: {}", greet_generic(&casual));Types must mix in one list: pick the trait object
Formal and Casual are different types, yet one Vec holds both — each behind its own vtable:
let crowd: Vec<Box<dyn Greeter>> = vec![Box::new(Formal), Box::new(Casual)];
for g in &crowd {
println!("crowd: {}", g.greet());
}A closed family: pick the enum
When every kind is known, one enum names them all. greet matches on the kind and calls that kind's own method — no vtable anywhere:
enum GreeterKind {
Formal(Formal),
Casual(Casual),
}
impl GreeterKind {
fn greet(&self) -> String {
match self {
GreeterKind::Formal(g) => g.greet(),
GreeterKind::Casual(g) => g.greet(),
}
}
}Example
trait Greeter {
fn greet(&self) -> String;
}
struct Formal;
struct Casual;
impl Greeter for Formal {
fn greet(&self) -> String { String::from("Good day to you.") }
}
impl Greeter for Casual {
fn greet(&self) -> String { String::from("Hey there!") }
}
fn greet_generic<G: Greeter>(greeter: &G) -> String {
greeter.greet()
}
fn greet_object(greeter: &dyn Greeter) -> String {
greeter.greet()
}
enum GreeterKind {
Formal(Formal),
Casual(Casual),
}
impl GreeterKind {
fn greet(&self) -> String {
match self {
GreeterKind::Formal(g) => g.greet(),
GreeterKind::Casual(g) => g.greet(),
}
}
}
fn main() {
let formal = Formal;
let casual = Casual;
println!("generic: {}", greet_generic(&casual));
let picked: &dyn Greeter = &formal;
println!("object: {}", greet_object(picked));
let kinds = vec![GreeterKind::Formal(Formal), GreeterKind::Casual(Casual)];
for kind in &kinds {
println!("enum: {}", kind.greet());
}
let crowd: Vec<Box<dyn Greeter>> = vec![Box::new(Formal), Box::new(Casual)];
for g in &crowd {
println!("crowd: {}", g.greet());
}
}Key points
- Question 1 — must different concrete types mix in one collection, field or return slot?
- If yes, only a trait object works: Box<dyn Trait> or &dyn Trait
- Question 2 — is the family of types closed, every kind known today and new kinds rare?
- If yes, an enum plus a match is the simplest tool, and the compiler checks every kind is handled
- Question 3 — is the type settled at each call site while compiling?
- Then a generic parameter gives direct calls, and the optimiser may inline them
- Question 4 — how hot is the call?
- A call through a vtable cannot be inlined.
- So in a tight inner loop, a generic or an enum usually wins.
- For set-up code and rare calls, the difference seldom matters.
- A generic trades binary size for speed: one compiled copy per type used, as the Rust Monomorphisation lesson shows
- A trait object trades one lookup per call for openness.
- Types written later, even by other people, join without touching your code.
- The trait must be object safe.
- Real libraries often accept generics at the edges and store trait objects inside — each tool where its strength counts.
- Choosing a dispatch style means picking which of the three ways a call uses to reach shared behaviour.
In this example, one job — greet — is done all three ways. greet_generic is called with the Casual value, and the compiler builds a version of it for Casual. greet_object receives &formal as a &dyn Greeter. The choice of type was made while running. The call travels through the vtable. The GreeterKind list mixes both kinds inside one enum type, and its greet matches on the kind. Last, the crowd Vec holds the two types as boxed trait objects, side by side. Every line prints the greeting its dispatch style was asked for. The answers agree. Only the machinery differs.
Note
These are not rivals for life.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A plugin system loads effect types written by other teams, unknown when your code was compiled. Which dispatch fits?