Dynamic Dispatch
Part of the Object Oriented Programming section of Coddy's Rust journey — lesson 45 of 61.
When you use generics with trait bounds like <T: Summary>, Rust determines the exact type at compile time. This is called static dispatch—the compiler generates specialized code for each concrete type you use. It's fast, but there's a limitation: a variable can only hold one specific type.
What if you need a single variable that could hold different types at runtime? This is where trait objects come in. Using Box<dyn Trait>, you can store any type that implements the trait:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}
The dyn keyword indicates dynamic dispatch—Rust looks up which method to call at runtime rather than compile time. The Box is necessary because trait objects don't have a known size; Box provides a pointer with a fixed size.
Think of it this way: generics say "I work with type T," while trait objects say "I work with anything that can do this." The tradeoff is a small runtime cost for the flexibility of handling different types through the same variable.
Challenge
EasyLet's build a vehicle rental system that demonstrates the power of dynamic dispatch! You'll create a trait that defines what any rentable vehicle can do, then use Box<dyn Trait> to store different vehicle types in the same variable.
You'll organize your code across two files:
vehicle.rs: Define a publicRentabletrait with arental_infomethod that takes&selfand returns aString. Then create two public structs:Car— with a publicmodelfield (String). Itsrental_infoshould returnCar: {model}Bike— with a publicbrandfield (String). Itsrental_infoshould returnBike: {brand}
main.rs: Bring in your vehicle module and use the inputs to demonstrate dynamic dispatch. Create aBox<dyn Rentable>variable that first holds aCar, print its rental info, then reassign the same variable to hold aBikeand print its rental info again.
The magic here is that a single variable of type Box<dyn Rentable> can hold either a Car or a Bike—Rust figures out which rental_info method to call at runtime. This is dynamic dispatch in action!
Your output should show both vehicles' information:
Car: {model}
Bike: {brand}For example, with inputs Tesla Model 3 and Trek:
Car: Tesla Model 3
Bike: TrekYou will receive two inputs: the car model and the bike brand.
Cheat sheet
Rust uses static dispatch with generics—the compiler generates specialized code for each type at compile time. This is fast but means a variable can only hold one specific type.
Trait objects enable dynamic dispatch, allowing a single variable to hold different types at runtime. Use Box<dyn Trait> to store any type implementing the trait:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}The dyn keyword indicates dynamic dispatch—method calls are resolved at runtime. Box is required because trait objects don't have a known size at compile time.
Key difference: Generics say "I work with type T" (one specific type), while trait objects say "I work with anything that implements this trait" (multiple types through the same variable). The tradeoff is a small runtime cost for flexibility.
Try it yourself
mod vehicle;
use vehicle::{Rentable, Car, Bike};
fn main() {
// Read input
let mut car_model = String::new();
std::io::stdin().read_line(&mut car_model).expect("Failed to read line");
let car_model = car_model.trim().to_string();
let mut bike_brand = String::new();
std::io::stdin().read_line(&mut bike_brand).expect("Failed to read line");
let bike_brand = bike_brand.trim().to_string();
// TODO: Create a Box<dyn Rentable> variable that holds a Car
// Print its rental info using the rental_info() method
// TODO: Reassign the same variable to hold a Bike
// Print its rental info again
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Methods and Behavior
Intro Implementation BlocksThe Self ParameterMutable MethodsAssociated FunctionsMultiple Implementation BlocksMethod ChainingRecap - Rectangle Actions4Project: Virtual Pet
Defining the PetFeeding the Pet7Standard Traits
The Debug TraitThe Display TraitClone and CopyEquality TraitsRecap - Printable Point10Project: Document System
The Draw TraitText Component2Encapsulation and Modules
Modules BasicsThe Public KeywordPrivate FieldsGettersSettersRecap - Secure Locker5Generics
Generic StructsGeneric MethodsMultiple Generic TypesGeneric FunctionsRecap - Coordinate Point8Traits as Bounds
Trait Bounds SyntaxMultiple BoundsThe Where ClauseReturning Types with TraitsRecap - Generic Printer11Design Patterns in Rust
Newtype PatternCompositionThe Drop TraitFrom and IntoRecap - Smart Pointer Mock