076 — Trait Objects (Dynamic Dispatch)
Tutorial
The Problem
Trait objects (dyn Trait) enable runtime polymorphism in Rust — the ability to work with different types through a common interface without knowing the concrete type at compile time. They are Rust's answer to OOP inheritance and interface polymorphism: Vec<Box<dyn Shape>> can hold circles, rectangles, and triangles in one collection.
Dynamic dispatch via dyn Trait is used in plugin systems, event handlers, GUI widget trees, game entity systems, and any architecture requiring heterogeneous collections. The trade-off: dynamic dispatch has a small vtable lookup overhead but enables flexibility that static generics cannot provide.
🎯 Learning Outcomes
&dyn Trait and Box<dyn Trait> for dynamic dispatchdyn Trait (runtime polymorphism) vs generics <T: Trait> (compile-time monomorphization)dyn Trait cannot be used with non-object-safe traitsCode Example
#![allow(clippy::all)]
// 076: Trait Objects — dynamic dispatch with dyn Trait
use std::f64::consts::PI;
// Approach 1: Define trait and implementations
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
fn name(&self) -> &str {
"circle"
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> &str {
"rectangle"
}
}
// Approach 2: Using dyn Trait for polymorphism
fn describe(shape: &dyn Shape) -> String {
format!("{} with area {:.2}", shape.name(), shape.area())
}
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// Approach 3: Returning trait objects
fn make_shape(kind: &str) -> Box<dyn Shape> {
match kind {
"circle" => Box::new(Circle { radius: 5.0 }),
"rectangle" => Box::new(Rectangle {
width: 3.0,
height: 4.0,
}),
_ => Box::new(Circle { radius: 1.0 }),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle() {
let c = Circle { radius: 5.0 };
assert!((c.area() - 78.54).abs() < 0.01);
assert_eq!(c.name(), "circle");
}
#[test]
fn test_rectangle() {
let r = Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(r.area(), 12.0);
}
#[test]
fn test_dyn_dispatch() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle {
width: 3.0,
height: 4.0,
}),
];
assert!((total_area(&shapes) - 90.54).abs() < 0.01);
}
#[test]
fn test_make_shape() {
let s = make_shape("circle");
assert_eq!(s.name(), "circle");
}
}Key Differences
dyn Trait vs records**: Rust's vtable is automatic — define the trait, implement it, use dyn Trait. OCaml requires manually building record-of-functions vtables, or using the OO subset (#name).dyn Trait requires "object safety": no methods with Self return type, no generic methods. OCaml's record-of-functions approach has no such restriction.Box for ownership**: Box<dyn Trait> owns the object. &dyn Trait borrows it. OCaml's record-of-functions is always heap-allocated (via GC) — no explicit boxing.fn area<T: Shape>(s: &T) monomorphizes (separate code per type, fast). fn area(s: &dyn Shape) uses vtable (one code path, flexible). OCaml's records are always vtable-style.OCaml Approach
OCaml uses record-of-functions as its idiomatic "dynamic dispatch" (manually built vtable):
type shape = {
area : unit -> float;
name : unit -> string;
}
let circle r = {
area = (fun () -> Float.pi *. r *. r);
name = (fun () -> "circle");
}
let rectangle w h = {
area = (fun () -> w *. h);
name = (fun () -> "rectangle");
}
let describe s = Printf.printf "%s: %.2f\n" (s.name ()) (s.area ())
OCaml's OO subset (#method) provides an alternative with structural subtyping. The record-of-functions approach mirrors Rust's vtable more directly.
Full Source
#![allow(clippy::all)]
// 076: Trait Objects — dynamic dispatch with dyn Trait
use std::f64::consts::PI;
// Approach 1: Define trait and implementations
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
fn name(&self) -> &str {
"circle"
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> &str {
"rectangle"
}
}
// Approach 2: Using dyn Trait for polymorphism
fn describe(shape: &dyn Shape) -> String {
format!("{} with area {:.2}", shape.name(), shape.area())
}
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// Approach 3: Returning trait objects
fn make_shape(kind: &str) -> Box<dyn Shape> {
match kind {
"circle" => Box::new(Circle { radius: 5.0 }),
"rectangle" => Box::new(Rectangle {
width: 3.0,
height: 4.0,
}),
_ => Box::new(Circle { radius: 1.0 }),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle() {
let c = Circle { radius: 5.0 };
assert!((c.area() - 78.54).abs() < 0.01);
assert_eq!(c.name(), "circle");
}
#[test]
fn test_rectangle() {
let r = Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(r.area(), 12.0);
}
#[test]
fn test_dyn_dispatch() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle {
width: 3.0,
height: 4.0,
}),
];
assert!((total_area(&shapes) - 90.54).abs() < 0.01);
}
#[test]
fn test_make_shape() {
let s = make_shape("circle");
assert_eq!(s.name(), "circle");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle() {
let c = Circle { radius: 5.0 };
assert!((c.area() - 78.54).abs() < 0.01);
assert_eq!(c.name(), "circle");
}
#[test]
fn test_rectangle() {
let r = Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(r.area(), 12.0);
}
#[test]
fn test_dyn_dispatch() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle {
width: 3.0,
height: 4.0,
}),
];
assert!((total_area(&shapes) - 90.54).abs() < 0.01);
}
#[test]
fn test_make_shape() {
let s = make_shape("circle");
assert_eq!(s.name(), "circle");
}
}
Deep Comparison
Core Insight
Trait objects (dyn Trait) enable runtime polymorphism. The compiler generates a vtable for method dispatch. This is Rust's equivalent of OCaml's first-class modules or object system.
OCaml Approach
Rust Approach
dyn Trait behind a pointer (Box<dyn Trait>, &dyn Trait)Self in return positionComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Dynamic dispatch | Objects / first-class modules | dyn Trait |
| Pointer type | Implicit (GC) | Box<dyn T> / &dyn T |
| Type erasure | Yes | Yes (via vtable) |
| Overhead | Method lookup | Fat pointer + vtable |
Exercises
Plugin trait with name(&self) -> &str and execute(&self, input: &str) -> String. Build a PluginRegistry that stores Vec<Box<dyn Plugin>> and dispatches by name.Clone as a trait object: &dyn Clone. Observe the compiler error. Explain why Clone is not object-safe and how to work around it with a CloneBoxed trait.area via &dyn Shape (dynamic dispatch) vs fn area<T: Shape>(s: &T) (static dispatch) on 10M calls. Quantify the vtable overhead.