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 and Circle implement the Shape trait with a method area.
  • We create instances of Rectangle and Circle and store them as trait objects (Box<dyn Shape>).
  • The shapes vector can hold any type implementing Shape 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.

Trait objects and dynamic dispatch

dynamic dispatch

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=9fcb319c8435d783642dabbee778c8c2

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.

Previous article

Arc::Clone

Next article

Lazy Iterators in Python