870-trait-objects — Trait Objects (Dynamic Dispatch)
Tutorial
The Problem
In object-oriented languages, polymorphism is the default: a method call on a base-class pointer dispatches to the concrete implementation at runtime. Functional languages like OCaml achieve the same through algebraic types with pattern matching (static) or object types (dynamic). Rust offers both mechanisms: generics with trait bounds (static/monomorphized dispatch, zero cost) and trait objects dyn Trait (dynamic dispatch via vtable, runtime polymorphism). The choice between them affects binary size, flexibility, and whether the concrete type must be known at compile time. This example shows both approaches using a Shape hierarchy.
🎯 Learning Outcomes
impl Trait) and dynamic dispatch (dyn Trait)Box<dyn Trait> or &dyn TraitCode Example
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { PI * self.radius * self.radius }
fn name(&self) -> &str { "Circle" }
}
fn total_area(shapes: &[&dyn Shape]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}Key Differences
&dyn Trait is two words (data + vtable); OCaml object values carry a vtable inline in the heap block header.Self return); OCaml has no such restriction.Vec<Box<dyn Shape>>-style collections; OCaml does it with shape list using object types.OCaml Approach
OCaml provides two mechanisms. Object types (class type shape = object method area: float ... end) give structural subtyping and dynamic dispatch. Algebraic types with pattern matching (type shape_adt = Circle of float | Rectangle of float * float | Triangle of float * float) give static exhaustive dispatch. The OCaml algebraic approach is more idiomatic for closed hierarchies; object types are used when the hierarchy must be extensible by third parties. Both compile to efficient code; object dispatch uses a vtable like Rust's dyn Trait.
Full Source
#![allow(clippy::all)]
// Example 076: Trait Objects — Dynamic Dispatch
// OCaml polymorphism → Rust dyn Trait vs generics
use std::f64::consts::PI;
// === Approach 1: Trait objects (dyn Trait) — dynamic dispatch ===
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
struct Triangle {
base: 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"
}
}
impl Shape for Triangle {
fn area(&self) -> f64 {
0.5 * self.base * self.height
}
fn name(&self) -> &str {
"Triangle"
}
}
// Dynamic dispatch: accepts any Shape via trait object
fn total_area_dyn(shapes: &[&dyn Shape]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn describe_dyn(shape: &dyn Shape) -> String {
format!("{}: area={:.2}", shape.name(), shape.area())
}
// === Approach 2: Generics (static dispatch / monomorphization) ===
fn describe_generic<S: Shape>(shape: &S) -> String {
format!("{}: area={:.2}", shape.name(), shape.area())
}
fn total_area_generic(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// === Approach 3: Enum dispatch (like OCaml ADT) ===
enum ShapeEnum {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64),
}
impl ShapeEnum {
fn area(&self) -> f64 {
match self {
ShapeEnum::Circle(r) => PI * r * r,
ShapeEnum::Rectangle(w, h) => w * h,
ShapeEnum::Triangle(b, h) => 0.5 * b * h,
}
}
fn name(&self) -> &str {
match self {
ShapeEnum::Circle(_) => "Circle",
ShapeEnum::Rectangle(_, _) => "Rectangle",
ShapeEnum::Triangle(_, _) => "Triangle",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle_area() {
let c = Circle { radius: 5.0 };
assert!((c.area() - PI * 25.0).abs() < 1e-10);
}
#[test]
fn test_rectangle_area() {
let r = Rectangle {
width: 3.0,
height: 4.0,
};
assert!((r.area() - 12.0).abs() < 1e-10);
}
#[test]
fn test_triangle_area() {
let t = Triangle {
base: 6.0,
height: 3.0,
};
assert!((t.area() - 9.0).abs() < 1e-10);
}
#[test]
fn test_dynamic_dispatch_total() {
let c = Circle { radius: 5.0 };
let r = Rectangle {
width: 3.0,
height: 4.0,
};
let t = Triangle {
base: 6.0,
height: 3.0,
};
let shapes: Vec<&dyn Shape> = vec![&c, &r, &t];
let total = total_area_dyn(&shapes);
let expected = PI * 25.0 + 12.0 + 9.0;
assert!((total - expected).abs() < 1e-10);
}
#[test]
fn test_enum_dispatch() {
let c = ShapeEnum::Circle(5.0);
assert!((c.area() - PI * 25.0).abs() < 1e-10);
assert_eq!(c.name(), "Circle");
}
#[test]
fn test_describe() {
let c = Circle { radius: 1.0 };
let desc = describe_generic(&c);
assert!(desc.starts_with("Circle"));
}
#[test]
fn test_boxed_shapes() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rectangle {
width: 2.0,
height: 3.0,
}),
];
let total = total_area_generic(&shapes);
assert!((total - (PI + 6.0)).abs() < 1e-10);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle_area() {
let c = Circle { radius: 5.0 };
assert!((c.area() - PI * 25.0).abs() < 1e-10);
}
#[test]
fn test_rectangle_area() {
let r = Rectangle {
width: 3.0,
height: 4.0,
};
assert!((r.area() - 12.0).abs() < 1e-10);
}
#[test]
fn test_triangle_area() {
let t = Triangle {
base: 6.0,
height: 3.0,
};
assert!((t.area() - 9.0).abs() < 1e-10);
}
#[test]
fn test_dynamic_dispatch_total() {
let c = Circle { radius: 5.0 };
let r = Rectangle {
width: 3.0,
height: 4.0,
};
let t = Triangle {
base: 6.0,
height: 3.0,
};
let shapes: Vec<&dyn Shape> = vec![&c, &r, &t];
let total = total_area_dyn(&shapes);
let expected = PI * 25.0 + 12.0 + 9.0;
assert!((total - expected).abs() < 1e-10);
}
#[test]
fn test_enum_dispatch() {
let c = ShapeEnum::Circle(5.0);
assert!((c.area() - PI * 25.0).abs() < 1e-10);
assert_eq!(c.name(), "Circle");
}
#[test]
fn test_describe() {
let c = Circle { radius: 1.0 };
let desc = describe_generic(&c);
assert!(desc.starts_with("Circle"));
}
#[test]
fn test_boxed_shapes() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rectangle {
width: 2.0,
height: 3.0,
}),
];
let total = total_area_generic(&shapes);
assert!((total - (PI + 6.0)).abs() < 1e-10);
}
}
Deep Comparison
Comparison: Trait Objects
Dynamic Dispatch
OCaml — Object types:
class type shape = object
method area : float
method name : string
end
class circle r = object
method area = Float.pi *. r *. r
method name = "Circle"
end
let total_area (shapes : shape list) =
List.fold_left (fun acc s -> acc +. s#area) 0.0 shapes
Rust — dyn Trait:
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { PI * self.radius * self.radius }
fn name(&self) -> &str { "Circle" }
}
fn total_area(shapes: &[&dyn Shape]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
Enum-Based (ADT) Dispatch
OCaml:
type shape = Circle of float | Rectangle of float * float
let area = function
| Circle r -> Float.pi *. r *. r
| Rectangle (w, h) -> w *. h
Rust:
enum Shape { Circle(f64), Rectangle(f64, f64) }
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => PI * r * r,
Shape::Rectangle(w, h) => w * h,
}
}
}
Static Dispatch (Generics)
OCaml — Functor/constraint:
module type SHAPE = sig
type t
val area : t -> float
end
Rust — Generic bounds:
fn describe<S: Shape>(shape: &S) -> String {
format!("{}: area={:.2}", shape.name(), shape.area())
}
Exercises
Perimeter trait and implement it for all shapes, then compute total perimeter of a Vec<Box<dyn Shape>>.describe_all function that takes &[Box<dyn Shape>] and returns a formatted summary using Display.total_area_dyn to use impl Iterator<Item = &dyn Shape> instead of a slice, and explain the difference.