Trait Objects and dynamic dispatch
Dynamic dispatch in Rust allows treating different types implementing a trait uniformly through trait objects like Box<dyn Trait>
. It provides flexibility by resolving method calls at runtime, enabling runtime polymorphism. This is particularly useful when dealing with collections of diverse types sharing a common trait.
“This works differently from defining a struct that uses a generic type parameter with trait bounds. A generic type parameter can only be substituted with one concrete type at a time, whereas trait objects allow for multiple concrete types to fill in for the trait object at runtime”
https://doc.rust-lang.org/book/ch17-02-trait-objects.html
trait Shape {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
// Using trait objects for dynamic dispatch
let rectangle: Box<dyn Shape> = Box::new(Rectangle {
width: 5.0,
height: 3.0,
});
let circle: Box<dyn Shape> = Box::new(Circle { radius: 2.0 });
// Storing different shapes in a vector using trait objects
let shapes: Vec<Box<dyn Shape>> = vec![rectangle, circle];
// Accessing methods through trait objects
for shape in shapes {
println!("Area: {}", shape.area());
}
}
- Both
Rectangle
andCircle
implement theShape
trait with a methodarea
. - We create instances of
Rectangle
andCircle
and store them as trait objects (Box<dyn Shape>
). - The
shapes
vector can hold any type implementingShape
due to the use of trait objects. - By using trait objects, we can store different types in the same vector and call the common
area
method without knowing the concrete types at compile time. This demonstrates the need for dynamic dispatch and the use of trait objects in scenarios where you want to work with different types through a common trait interface.
Dynamic Dispatch v Generics
Generics:
- Compile-time polymorphism.
- Static dispatch: The actual code for different types is generated at compile time.
- Requires knowing types at compile time.
Trait Objects:
- Runtime polymorphism.
- Dynamic dispatch: Method calls are resolved at runtime.
- Supports different types at runtime through trait objects.
We’ve just looked at dynamic dispatch!
Trait objects and dynamic dispatch
The choice between using generics or trait objects depends on your specific use case and requirements. Generics provide static dispatch, while trait objects offer dynamic dispatch.
dynamic dispatch
Summary
Dynamic dispatch is useful when dealing with varying concrete types that implement a shared trait. It allows handling different types through trait objects, enabling runtime flexibility and polymorphism. This is advantageous in scenarios where the specific implementation is determined dynamically, like creating extensible plugins or building generic data structures. However, it comes with a runtime performance cost compared to static dispatch, where the specific method is known at compile-time. Use dynamic dispatch when runtime type flexibility is crucial and the performance trade-off is acceptable, such as in GUI frameworks or plugin architectures.