Guide contents

Lesson 33 of 94

Trait Objects & Dynamic Dispatch

Generics give you static dispatch: one specialized copy of the code per type, decided at compile time. That can't handle a Vec holding circles and squares, or a function that returns different types depending on runtime input.

Trait objects give you dynamic dispatch: a value of type dyn Shape is "some type implementing Shape", and each method call looks up the right implementation at runtime.

Use case Rust
A collection holding circles and squares Vec<Box<dyn Shape>>
A boxed shape value let s: Box<dyn Shape> = Box::new(Circle(1.0));
A function taking any shape fn draw(s: &dyn Shape)
Which calls are virtual Only calls through dyn Trait are virtual
Recovering the concrete type Any::downcast_ref (rarely needed)

1. The problem: a list of different types

A Vec<T> holds values of one type. A circle and a square are different types, even if both implement Shape.

Rust✗ does not compile · E0308
trait Shape {
    fn area(&self) -> f64;
}

struct Circle(f64);
struct Square(f64);

impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.0 * self.0 }
}

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
}

fn main() {
    let shapes = vec![Circle(1.0), Square(2.0)];
}
Output
error[E0308]: mismatched types
  --> src/main.rs:17:36
   |
17 |     let shapes = vec![Circle(1.0), Square(2.0)];
   |                                    ^^^^^^^^^^^ expected `Circle`, found `Square`

2. Box<dyn Shape>: a list of trait objects

Box::new(value) puts the value on the heap (see Box), and Box<dyn Shape> is a pointer to "some Shape". All the boxes have the same size, so they fit in one Vec.

Rust
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Circle(f64);
struct Square(f64);

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.0 * self.0
    }
    fn name(&self) -> String {
        format!("circle r={}", self.0)
    }
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
    fn name(&self) -> String {
        format!("square s={}", self.0)
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Square(2.0)), Box::new(Circle(0.5))];
    let mut total = 0.0;
    for s in &shapes {
        println!("{:<14} {:.3}", s.name(), s.area());
        total += s.area();
    }
    println!("total {:.3}", total);
}
Output
circle r=1     3.142
square s=2     4.000
circle r=0.5   0.785
total 7.927

3. &dyn Trait parameters

A function can take a reference to a trait object. Unlike a generic function, only one copy of it is compiled, and it works with any implementing type.

Rust
trait Describe {
    fn describe(&self) -> String;
}

struct User {
    name: String,
}

struct Server {
    host: String,
    port: u16,
}

impl Describe for User {
    fn describe(&self) -> String {
        format!("user {}", self.name)
    }
}

impl Describe for Server {
    fn describe(&self) -> String {
        format!("server {}:{}", self.host, self.port)
    }
}

fn log(item: &dyn Describe) {
    println!("[log] {}", item.describe());
}

fn main() {
    let u = User { name: String::from("ada") };
    let s = Server { host: String::from("10.0.0.5"), port: 443 };
    log(&u);
    log(&s);
}
Output
[log] user ada
[log] server 10.0.0.5:443

4. Returning different types: Box<dyn Trait>

This fixes the impl Trait error from Trait Bounds: the function can return a different concrete type on each path.

Rust
trait Shape {
    fn area(&self) -> f64;
}

struct Circle(f64);
struct Square(f64);

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.0 * self.0
    }
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
}

fn make(kind: &str, size: f64) -> Option<Box<dyn Shape>> {
    match kind {
        "circle" => Some(Box::new(Circle(size))),
        "square" => Some(Box::new(Square(size))),
        _ => None,
    }
}

fn main() {
    for (kind, size) in [("circle", 2.0), ("square", 3.0), ("hexagon", 1.0)] {
        match make(kind, size) {
            Some(shape) => println!("{kind}: {:.2}", shape.area()),
            None => println!("{kind}: unknown shape"),
        }
    }
}
Output
circle: 12.57
square: 9.00
hexagon: unknown shape

5. What a trait object looks like in memory

A &dyn Shape or Box<dyn Shape> is a fat pointer: one pointer to the data, and one pointer to a vtable, a table of function pointers for that type's implementation of the trait. A method call reads the function's address from the vtable, then calls it.

Text
  Box<dyn Shape>              heap                    vtable for Circle as Shape
 ┌──────────────┐          ┌──────────┐             ┌───────────────────────┐
 │ data ptr   ──┼────────► │ Circle   │             │ drop_in_place::<Circle>│
 │ vtable ptr ──┼────┐     │  radius  │             │ size, align            │
 └──────────────┘    │     └──────────┘             │ area  -> Circle::area  │
                     └─────────────────────────────►│ name  -> Circle::name  │
                                                    └───────────────────────┘
Rust
trait Shape {
    fn area(&self) -> f64;
}

struct Circle(f64);

impl Shape for Circle {
    fn area(&self) -> f64 {
        self.0
    }
}

fn main() {
    println!("&Circle:        {} bytes", std::mem::size_of::<&Circle>());
    println!("&dyn Shape:     {} bytes", std::mem::size_of::<&dyn Shape>());
    println!("Box<dyn Shape>: {} bytes", std::mem::size_of::<Box<dyn Shape>>());
    let c = Circle(2.0);
    let s: &dyn Shape = &c;
    println!("area via vtable: {}", s.area());
}
Output
&Circle:        8 bytes
&dyn Shape:     16 bytes
Box<dyn Shape>: 16 bytes
area via vtable: 2

6. Static dispatch vs dynamic dispatch

The same trait can be used both ways. Pick per call site.

Rust
trait Tax {
    fn apply(&self, cents: u64) -> u64;
}

struct Vat(u64);
struct Exempt;

impl Tax for Vat {
    fn apply(&self, cents: u64) -> u64 {
        cents + cents * self.0 / 100
    }
}

impl Tax for Exempt {
    fn apply(&self, cents: u64) -> u64 {
        cents
    }
}

// Static: compiled once per T, calls are direct and can be inlined.
fn total_static<T: Tax>(tax: &T, items: &[u64]) -> u64 {
    items.iter().map(|&i| tax.apply(i)).sum()
}

// Dynamic: compiled once, each call goes through the vtable.
fn total_dynamic(tax: &dyn Tax, items: &[u64]) -> u64 {
    items.iter().map(|&i| tax.apply(i)).sum()
}

fn main() {
    let items = [1_000, 2_000];
    println!("{} {}", total_static(&Vat(20), &items), total_static(&Exempt, &items));
    println!("{} {}", total_dynamic(&Vat(20), &items), total_dynamic(&Exempt, &items));

    let taxes: Vec<Box<dyn Tax>> = vec![Box::new(Vat(20)), Box::new(Exempt), Box::new(Vat(5))];
    for t in &taxes {
        print!("{} ", total_dynamic(t.as_ref(), &items));
    }
    println!();
}
Output
3600 3000
3600 3000
3600 3000 3150
Generics (T: Trait, impl Trait) Trait objects (dyn Trait)
Resolved At compile time At runtime, through a vtable
Mixed types in one collection No Yes
Code size One copy per type One copy total
Call cost Direct call, inlinable Indirect call, usually not inlined
Can use generic methods of the trait Yes No

t.as_ref() turns a &Box<dyn Tax> into a &dyn Tax.

7. Default methods work through dyn

Rust
trait Greeter {
    fn name(&self) -> String;
    fn greet(&self) -> String {
        format!("Hello, {}!", self.name())
    }
}

struct Formal;
struct Casual;

impl Greeter for Formal {
    fn name(&self) -> String {
        String::from("Dr. Hopper")
    }
    fn greet(&self) -> String {
        format!("Good evening, {}.", self.name())
    }
}

impl Greeter for Casual {
    fn name(&self) -> String {
        String::from("Grace")
    }
}

fn main() {
    let greeters: [&dyn Greeter; 2] = [&Formal, &Casual];
    for g in greeters {
        println!("{}", g.greet());
    }
}
Output
Good evening, Dr. Hopper.
Hello, Grace!

8. &mut dyn Trait for mutable behavior

Rust
trait Counter {
    fn record(&mut self, value: u64);
    fn summary(&self) -> String;
}

struct Sum(u64);
struct Max(u64);

impl Counter for Sum {
    fn record(&mut self, value: u64) {
        self.0 += value;
    }
    fn summary(&self) -> String {
        format!("sum={}", self.0)
    }
}

impl Counter for Max {
    fn record(&mut self, value: u64) {
        self.0 = self.0.max(value);
    }
    fn summary(&self) -> String {
        format!("max={}", self.0)
    }
}

fn feed(counter: &mut dyn Counter, values: &[u64]) {
    for &v in values {
        counter.record(v);
    }
}

fn main() {
    let mut counters: Vec<Box<dyn Counter>> = vec![Box::new(Sum(0)), Box::new(Max(0))];
    for c in counters.iter_mut() {
        feed(c.as_mut(), &[12, 7, 30, 1]);
    }
    for c in &counters {
        println!("{}", c.summary());
    }
}
Output
sum=50
max=30

9. Dyn compatibility: generic methods aren't allowed

A vtable holds one function pointer per method. A generic method would need one entry for every possible type argument, which is impossible. So a trait with a generic method can't be used as dyn Trait. (This used to be called object safety.)

Rust✗ does not compile · E0038
trait Logger {
    fn log<T: std::fmt::Debug>(&self, value: T);
}

struct Console;

impl Logger for Console {
    fn log<T: std::fmt::Debug>(&self, value: T) {
        println!("{:?}", value);
    }
}

fn main() {
    let l: Box<dyn Logger> = Box::new(Console);
}
Output
error[E0038]: the trait `Logger` is not dyn compatible
  --> src/main.rs:14:20
   |
14 |     let l: Box<dyn Logger> = Box::new(Console);
   |                    ^^^^^^ `Logger` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> src/main.rs:2:8
   |
 1 | trait Logger {
   |       ------ this trait is not dyn compatible...
 2 |     fn log<T: std::fmt::Debug>(&self, value: T);
   |        ^^^ ...because method `log` has generic type parameters
   = help: consider moving `log` to another trait
   = help: only type `Console` implements `Logger`; consider using it directly instead.

10. Dyn compatibility: methods that return Self

A method returning Self by value can't be called through dyn Trait, because the caller doesn't know the concrete type or its size. The trait Clone is the famous example: Box<dyn Clone> is not allowed.

Rust✗ does not compile · E0038
trait Shape {
    fn area(&self) -> f64;
    fn duplicate(&self) -> Self;
}

struct Square(f64);

impl Shape for Square {
    fn area(&self) -> f64 { self.0 * self.0 }
    fn duplicate(&self) -> Self { Square(self.0) }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square(1.0))];
}
Output
error[E0038]: the trait `Shape` is not dyn compatible
  --> src/main.rs:14:29
   |
14 |     let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square(1.0))];
   |                             ^^^^^ `Shape` is not dyn compatible
   |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
  --> src/main.rs:3:28
   |
 1 | trait Shape {
   |       ----- this trait is not dyn compatible...
 2 |     fn area(&self) -> f64;
 3 |     fn duplicate(&self) -> Self;
   |                            ^^^^ ...because method `duplicate` references the `Self` type in its return type
   = help: consider moving `duplicate` to another trait
   = help: only type `Square` implements `Shape`; consider using it directly instead.

11. The escape hatch: where Self: Sized

Marking a method where Self: Sized excludes it from the vtable. The trait becomes dyn-compatible, and the method is still available on concrete types.

Rust
trait Shape {
    fn area(&self) -> f64;
    fn duplicate(&self) -> Self
    where
        Self: Sized;
}

#[derive(Debug)]
struct Square(f64);

impl Shape for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
    fn duplicate(&self) -> Self {
        Square(self.0)
    }
}

fn main() {
    let sq = Square(3.0);
    println!("{:?}", sq.duplicate()); // fine on the concrete type
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(sq)];
    println!("{}", shapes[0].area()); // fine through dyn
}
Output
Square(3.0)
9

12. Cloning a Box<dyn Trait>

Since Clone isn't dyn-compatible, the standard trick is a clone_box method that returns Box<dyn Trait>, plus an impl Clone for Box<dyn Trait> that calls it.

Rust
trait Shape {
    fn area(&self) -> f64;
    fn clone_box(&self) -> Box<dyn Shape>;
}

#[derive(Clone)]
struct Circle(f64);

#[derive(Clone)]
struct Square(f64);

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.0 * self.0
    }
    fn clone_box(&self) -> Box<dyn Shape> {
        Box::new(self.clone())
    }
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
    fn clone_box(&self) -> Box<dyn Shape> {
        Box::new(self.clone())
    }
}

impl Clone for Box<dyn Shape> {
    fn clone(&self) -> Self {
        self.clone_box()
    }
}

fn main() {
    let original: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Square(2.0))];
    let copy = original.clone(); // Vec::clone works because Box<dyn Shape>: Clone
    let areas: Vec<String> = copy.iter().map(|s| format!("{:.2}", s.area())).collect();
    println!("{:?} (from {} originals)", areas, original.len());
}
Output
["3.14", "4.00"] (from 2 originals)

13. Supertraits make trait objects printable

A trait object only has the methods of its trait. To print one with {:?}, make Debug a supertrait: every Shape must also be Debug, so dyn Shape can be printed.

Rust
use std::fmt::Debug;

trait Shape: Debug {
    fn area(&self) -> f64;
}

#[derive(Debug)]
struct Rect {
    w: f64,
    h: f64,
}

#[derive(Debug)]
struct Dot;

impl Shape for Rect {
    fn area(&self) -> f64 {
        self.w * self.h
    }
}

impl Shape for Dot {
    fn area(&self) -> f64 {
        0.0
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Rect { w: 2.0, h: 3.0 }), Box::new(Dot)];
    println!("{:?}", shapes);
}
Output
[Rect { w: 2.0, h: 3.0 }, Dot]

14. Upcasting: from dyn Sub to dyn Super

Since Rust 1.86, a trait object converts to a trait object of any of its supertraits: &dyn Shape becomes &dyn Debug when Shape: Debug. The most useful case is Any. A component system declares trait Component: Any, stores Box<dyn Component>, and upcasts to &dyn Any to downcast back to the concrete type:

Rust
use std::any::Any;
use std::fmt::Debug;

trait Component: Any + Debug {
    fn name(&self) -> &'static str;
}

#[derive(Debug)]
struct Health(u32);

#[derive(Debug)]
struct Position {
    x: i32,
    y: i32,
}

impl Component for Health {
    fn name(&self) -> &'static str {
        "health"
    }
}

impl Component for Position {
    fn name(&self) -> &'static str {
        "position"
    }
}

fn log(value: &dyn Debug) {
    println!("log: {value:?}");
}

fn main() {
    let components: Vec<Box<dyn Component>> = vec![Box::new(Health(80)), Box::new(Position { x: 3, y: -1 })];

    for component in &components {
        log(component.as_ref()); // &dyn Component -> &dyn Debug

        let any: &dyn Any = component.as_ref(); // &dyn Component -> &dyn Any
        if let Some(Health(hp)) = any.downcast_ref::<Health>() {
            println!("{} is {hp}", component.name());
        } else if let Some(pos) = any.downcast_ref::<Position>() {
            println!("{} is ({}, {})", component.name(), pos.x, pos.y);
        }
    }
}
Output
log: Health(80)
health is 80
log: Position { x: 3, y: -1 }
position is (3, -1)

Code written before Rust 1.86 can't upcast, so you'll often see a helper method instead: fn as_any(&self) -> &dyn Any { self } in every implementation.

15. Downcasting with Any

Occasionally you need to get the concrete type back. std::any::Any supports it. The trait exposes an as_any method that returns &dyn Any, and downcast_ref::<T>() returns Some only if the type matches.

Rust
use std::any::Any;

trait Component {
    fn name(&self) -> &str;
    fn as_any(&self) -> &dyn Any;
}

struct Button {
    label: String,
}

struct Slider {
    value: u8,
}

impl Component for Button {
    fn name(&self) -> &str {
        "button"
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl Component for Slider {
    fn name(&self) -> &str {
        "slider"
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let ui: Vec<Box<dyn Component>> = vec![
        Box::new(Button { label: String::from("Save") }),
        Box::new(Slider { value: 70 }),
    ];
    for c in &ui {
        if let Some(b) = c.as_any().downcast_ref::<Button>() {
            println!("{} with label '{}'", c.name(), b.label);
        } else if let Some(s) = c.as_any().downcast_ref::<Slider>() {
            println!("{} at {}%", c.name(), s.value);
        }
    }
}
Output
button with label 'Save'
slider at 70%

Needing to downcast is usually a sign the trait is missing a method, or that an enum would fit better (example 21).

16. A trait object as a struct field

Holding a Box<dyn Trait> in a struct lets the behavior be chosen at runtime, which is dependency injection without a framework.

Rust
trait Clock {
    fn now_ms(&self) -> u64;
}

struct FixedClock(u64);

impl Clock for FixedClock {
    fn now_ms(&self) -> u64 {
        self.0
    }
}

struct SessionService {
    clock: Box<dyn Clock>,
    ttl_ms: u64,
}

impl SessionService {
    fn is_expired(&self, created_at_ms: u64) -> bool {
        self.clock.now_ms() - created_at_ms > self.ttl_ms
    }
}

fn main() {
    let service = SessionService { clock: Box::new(FixedClock(10_000)), ttl_ms: 3_000 };
    println!("{}", service.is_expired(8_000));
    println!("{}", service.is_expired(5_000));
}
Output
false
true

In tests you inject a fixed clock, and in production a real one.

17. Trait objects that borrow

A trait object can hold references. By default, Box<dyn Trait> means Box<dyn Trait + 'static> (it may not borrow anything short-lived). Add a lifetime to allow borrowing: Box<dyn Trait + 'a>.

Rust
trait Filter {
    fn keep(&self, line: &str) -> bool;
}

struct Contains<'a> {
    needle: &'a str,
}

impl Filter for Contains<'_> {
    fn keep(&self, line: &str) -> bool {
        line.contains(self.needle)
    }
}

fn build_filter<'a>(needle: &'a str) -> Box<dyn Filter + 'a> {
    Box::new(Contains { needle })
}

fn main() {
    let search_term = String::from("ERROR");
    let filter = build_filter(&search_term);
    for line in ["INFO ok", "ERROR disk", "WARN slow", "ERROR net"] {
        if filter.keep(line) {
            println!("{line}");
        }
    }
}
Output
ERROR disk
ERROR net

18. Realistic example: validation rules as a list

This finishes the password rules from Traits: rules of different types in one Vec, checked in a loop.

Rust
trait Rule {
    fn check(&self, password: &str) -> Result<(), String>;
}

struct MinLength(usize);
struct RequiresDigit;
struct RequiresSymbol;
struct NotIn(Vec<&'static str>);

impl Rule for MinLength {
    fn check(&self, p: &str) -> Result<(), String> {
        if p.chars().count() >= self.0 { Ok(()) } else { Err(format!("at least {} characters", self.0)) }
    }
}

impl Rule for RequiresDigit {
    fn check(&self, p: &str) -> Result<(), String> {
        if p.chars().any(|c| c.is_ascii_digit()) { Ok(()) } else { Err(String::from("needs a digit")) }
    }
}

impl Rule for RequiresSymbol {
    fn check(&self, p: &str) -> Result<(), String> {
        if p.chars().any(|c| !c.is_alphanumeric()) { Ok(()) } else { Err(String::from("needs a symbol")) }
    }
}

impl Rule for NotIn {
    fn check(&self, p: &str) -> Result<(), String> {
        if self.0.contains(&p.to_lowercase().as_str()) { Err(String::from("too common")) } else { Ok(()) }
    }
}

fn main() {
    let rules: Vec<Box<dyn Rule>> = vec![
        Box::new(MinLength(10)),
        Box::new(RequiresDigit),
        Box::new(RequiresSymbol),
        Box::new(NotIn(vec!["password1!", "qwerty123!"])),
    ];
    for pw in ["short", "Password1!", "correct-horse-7"] {
        let errors: Vec<String> = rules.iter().filter_map(|r| r.check(pw).err()).collect();
        println!("{pw:>16}: {}", if errors.is_empty() { String::from("OK") } else { errors.join(", ") });
    }
}
Output
           short: at least 10 characters, needs a digit, needs a symbol
      Password1!: too common
 correct-horse-7: OK

19. Realistic example: a command registry

A map from command names to handlers is the classic plugin pattern. New commands are added without touching the dispatcher.

Rust
use std::collections::HashMap;

trait Command {
    fn run(&self, args: &[&str]) -> String;
    fn help(&self) -> &'static str;
}

struct Echo;
struct Add;
struct Upper;

impl Command for Echo {
    fn run(&self, args: &[&str]) -> String {
        args.join(" ")
    }
    fn help(&self) -> &'static str {
        "print the arguments"
    }
}

impl Command for Add {
    fn run(&self, args: &[&str]) -> String {
        let total: i64 = args.iter().filter_map(|a| a.parse::<i64>().ok()).sum();
        total.to_string()
    }
    fn help(&self) -> &'static str {
        "add numbers"
    }
}

impl Command for Upper {
    fn run(&self, args: &[&str]) -> String {
        args.join(" ").to_uppercase()
    }
    fn help(&self) -> &'static str {
        "uppercase the arguments"
    }
}

fn main() {
    let mut commands: HashMap<&str, Box<dyn Command>> = HashMap::new();
    commands.insert("echo", Box::new(Echo));
    commands.insert("add", Box::new(Add));
    commands.insert("upper", Box::new(Upper));

    for line in ["echo hello world", "add 2 40 x 3", "upper rust traits", "rm -rf /"] {
        let parts: Vec<&str> = line.split_whitespace().collect();
        let (name, args) = parts.split_first().unwrap();
        match commands.get(name) {
            Some(cmd) => println!("{line:<20} => {}", cmd.run(args)),
            None => println!("{line:<20} => unknown command"),
        }
    }
    let mut names: Vec<&&str> = commands.keys().collect();
    names.sort();
    for n in names {
        println!("{n:>6}: {}", commands[n].help());
    }
}
Output
echo hello world     => hello world
add 2 40 x 3         => 45
upper rust traits    => RUST TRAITS
rm -rf /             => unknown command
   add: add numbers
  echo: print the arguments
 upper: uppercase the arguments

20. Realistic example: the observer pattern

Rust
trait Observer {
    fn notify(&mut self, event: &str);
    fn report(&self) -> String;
}

struct AuditLog {
    entries: Vec<String>,
}

struct AlertCounter {
    alerts: u32,
}

impl Observer for AuditLog {
    fn notify(&mut self, event: &str) {
        self.entries.push(event.to_string());
    }
    fn report(&self) -> String {
        format!("audit log: {} entries", self.entries.len())
    }
}

impl Observer for AlertCounter {
    fn notify(&mut self, event: &str) {
        if event.starts_with("ALERT") {
            self.alerts += 1;
        }
    }
    fn report(&self) -> String {
        format!("alerts: {}", self.alerts)
    }
}

struct Subject {
    observers: Vec<Box<dyn Observer>>,
}

impl Subject {
    fn emit(&mut self, event: &str) {
        for o in &mut self.observers {
            o.notify(event);
        }
    }
}

fn main() {
    let mut subject = Subject {
        observers: vec![Box::new(AuditLog { entries: Vec::new() }), Box::new(AlertCounter { alerts: 0 })],
    };
    for e in ["user login", "ALERT cpu 99%", "config changed", "ALERT disk full"] {
        subject.emit(e);
    }
    for o in &subject.observers {
        println!("{}", o.report());
    }
}
Output
audit log: 4 entries
alerts: 2

21. Enums vs trait objects

Both give you "one of several types". Choose based on whether the set of types is closed (known now) or open (others will add more):

  • Enum: closed set. You can add operations easily (just write a new match), and match is exhaustive. No heap allocation, no vtable.
  • Trait object: open set. Other code can add new types without changing yours, but adding a new operation means changing the trait and every implementation.
Rust
// Closed set: every payment method is known and handled in one place.
enum Payment {
    Card { last4: &'static str },
    Paypal { email: &'static str },
    Invoice { days: u32 },
}

impl Payment {
    fn describe(&self) -> String {
        match self {
            Payment::Card { last4 } => format!("card ending {last4}"),
            Payment::Paypal { email } => format!("PayPal {email}"),
            Payment::Invoice { days } => format!("invoice, net {days}"),
        }
    }
}

// Open set: plugins can add new exporters without changing this code.
trait Exporter {
    fn export(&self, rows: &[&str]) -> String;
}

struct Csv;
struct Json;

impl Exporter for Csv {
    fn export(&self, rows: &[&str]) -> String {
        rows.join(",")
    }
}

impl Exporter for Json {
    fn export(&self, rows: &[&str]) -> String {
        let quoted: Vec<String> = rows.iter().map(|r| format!("\"{r}\"")).collect();
        format!("[{}]", quoted.join(","))
    }
}

fn main() {
    let payments = [Payment::Card { last4: "4242" }, Payment::Paypal { email: "a@b.c" }, Payment::Invoice { days: 30 }];
    for p in &payments {
        println!("{}", p.describe());
    }
    let exporters: Vec<Box<dyn Exporter>> = vec![Box::new(Csv), Box::new(Json)];
    for e in &exporters {
        println!("{}", e.export(&["a", "b"]));
    }
}
Output
card ending 4242
PayPal a@b.c
invoice, net 30
a,b
["a","b"]

In Rust, reach for an enum first when the set of cases is yours to define.

22. Realistic example: HTTP middleware chain

Each middleware can inspect or modify the request, or stop it. The chain is a Vec<Box<dyn Middleware>> built at startup from configuration.

Rust
#[derive(Debug)]
struct Request {
    path: String,
    headers: Vec<(String, String)>,
}

trait Middleware {
    fn handle(&self, req: &mut Request) -> Result<(), String>;
}

struct RequireAuth;
struct AddRequestId(u32);
struct BlockPaths(Vec<&'static str>);

impl Middleware for RequireAuth {
    fn handle(&self, req: &mut Request) -> Result<(), String> {
        if req.headers.iter().any(|(k, _)| k == "Authorization") { Ok(()) } else { Err(String::from("401 unauthorized")) }
    }
}

impl Middleware for AddRequestId {
    fn handle(&self, req: &mut Request) -> Result<(), String> {
        req.headers.push((String::from("X-Request-Id"), format!("req-{}", self.0)));
        Ok(())
    }
}

impl Middleware for BlockPaths {
    fn handle(&self, req: &mut Request) -> Result<(), String> {
        if self.0.iter().any(|p| req.path.starts_with(p)) { Err(format!("403 forbidden: {}", req.path)) } else { Ok(()) }
    }
}

fn run(chain: &[Box<dyn Middleware>], mut req: Request) -> Result<Request, String> {
    for m in chain {
        m.handle(&mut req)?;
    }
    Ok(req)
}

fn main() {
    let chain: Vec<Box<dyn Middleware>> = vec![Box::new(AddRequestId(77)), Box::new(BlockPaths(vec!["/admin"])), Box::new(RequireAuth)];
    let ok = Request { path: String::from("/api/orders"), headers: vec![(String::from("Authorization"), String::from("Bearer x"))] };
    let no_auth = Request { path: String::from("/api/orders"), headers: vec![] };
    let admin = Request { path: String::from("/admin/users"), headers: vec![] };
    println!("{:?}", run(&chain, ok).map(|r| r.headers));
    println!("{:?}", run(&chain, no_auth).map(|r| r.path));
    println!("{:?}", run(&chain, admin).map(|r| r.path));
}
Output
Ok([("Authorization", "Bearer x"), ("X-Request-Id", "req-77")])
Err("401 unauthorized")
Err("403 forbidden: /admin/users")

23. Realistic example: payment processors chosen at runtime

Rust
trait PaymentProcessor {
    fn name(&self) -> &'static str;
    fn fee_cents(&self, amount_cents: u64) -> u64;
    fn charge(&self, amount_cents: u64) -> Result<String, String>;
}

struct Stripe;
struct BankTransfer {
    max_cents: u64,
}

impl PaymentProcessor for Stripe {
    fn name(&self) -> &'static str {
        "stripe"
    }
    fn fee_cents(&self, amount: u64) -> u64 {
        30 + amount * 29 / 1000
    }
    fn charge(&self, amount: u64) -> Result<String, String> {
        Ok(format!("ch_{amount}"))
    }
}

impl PaymentProcessor for BankTransfer {
    fn name(&self) -> &'static str {
        "bank"
    }
    fn fee_cents(&self, _amount: u64) -> u64 {
        0
    }
    fn charge(&self, amount: u64) -> Result<String, String> {
        if amount > self.max_cents { Err(format!("over bank limit of {}", self.max_cents)) } else { Ok(format!("bt_{amount}")) }
    }
}

fn processor_for(country: &str) -> Box<dyn PaymentProcessor> {
    match country {
        "DE" | "NL" => Box::new(BankTransfer { max_cents: 500_000 }),
        _ => Box::new(Stripe),
    }
}

fn main() {
    for (country, amount) in [("US", 10_000), ("DE", 10_000), ("NL", 900_000)] {
        let p = processor_for(country);
        match p.charge(amount) {
            Ok(id) => println!("{country}: {} charged {amount} (fee {}) -> {id}", p.name(), p.fee_cents(amount)),
            Err(e) => println!("{country}: {} failed: {e}", p.name()),
        }
    }
}
Output
US: stripe charged 10000 (fee 320) -> ch_10000
DE: bank charged 10000 (fee 0) -> bt_10000
NL: bank failed: over bank limit of 500000

24. Realistic example: a report built from pluggable sections

Rust
struct Data {
    orders: Vec<(u32, u64)>, // (id, cents)
    refunds: Vec<u64>,
}

trait Section {
    fn title(&self) -> String;
    fn body(&self, data: &Data) -> String;
}

struct Totals;
struct LargestOrder;
struct RefundRate;

impl Section for Totals {
    fn title(&self) -> String {
        String::from("Totals")
    }
    fn body(&self, d: &Data) -> String {
        let revenue: u64 = d.orders.iter().map(|(_, c)| c).sum();
        format!("{} orders, revenue {:.2}", d.orders.len(), revenue as f64 / 100.0)
    }
}

impl Section for LargestOrder {
    fn title(&self) -> String {
        String::from("Largest order")
    }
    fn body(&self, d: &Data) -> String {
        match d.orders.iter().max_by_key(|(_, c)| *c) {
            Some((id, c)) => format!("#{id} at {:.2}", *c as f64 / 100.0),
            None => String::from("none"),
        }
    }
}

impl Section for RefundRate {
    fn title(&self) -> String {
        String::from("Refund rate")
    }
    fn body(&self, d: &Data) -> String {
        format!("{:.1}%", d.refunds.len() as f64 * 100.0 / d.orders.len().max(1) as f64)
    }
}

fn render(sections: &[Box<dyn Section>], data: &Data) -> String {
    sections.iter().map(|s| format!("## {}\n{}", s.title(), s.body(data))).collect::<Vec<_>>().join("\n\n")
}

fn main() {
    let data = Data { orders: vec![(1, 4_500), (2, 129_900), (3, 2_000), (4, 9_950)], refunds: vec![2_000] };
    let sections: Vec<Box<dyn Section>> = vec![Box::new(Totals), Box::new(LargestOrder), Box::new(RefundRate)];
    println!("{}", render(&sections, &data));
}
Output
<h2 id="totals">Totals</h2>
4 orders, revenue 1463.50

<h2 id="largest-order">Largest order</h2>
#2 at 1299.00

<h2 id="refund-rate">Refund rate</h2>
25.0%

25. Realistic example: a game entity loop

A game loop updates many kinds of entities through one trait. Some entities die and are removed.

Rust
trait Entity {
    fn update(&mut self, tick: u32);
    fn alive(&self) -> bool;
    fn describe(&self) -> String;
}

struct Player {
    hp: i32,
    x: i32,
}

struct Projectile {
    x: i32,
    range_left: u32,
}

impl Entity for Player {
    fn update(&mut self, tick: u32) {
        self.x += 1;
        if tick == 2 {
            self.hp -= 30;
        }
    }
    fn alive(&self) -> bool {
        self.hp > 0
    }
    fn describe(&self) -> String {
        format!("player hp={} x={}", self.hp, self.x)
    }
}

impl Entity for Projectile {
    fn update(&mut self, _tick: u32) {
        self.x += 3;
        self.range_left = self.range_left.saturating_sub(1);
    }
    fn alive(&self) -> bool {
        self.range_left > 0
    }
    fn describe(&self) -> String {
        format!("projectile x={} range={}", self.x, self.range_left)
    }
}

fn main() {
    let mut world: Vec<Box<dyn Entity>> = vec![
        Box::new(Player { hp: 100, x: 0 }),
        Box::new(Projectile { x: 0, range_left: 2 }),
        Box::new(Projectile { x: 5, range_left: 4 }),
    ];
    for tick in 1..=3 {
        for e in world.iter_mut() {
            e.update(tick);
        }
        world.retain(|e| e.alive());
        let states: Vec<String> = world.iter().map(|e| e.describe()).collect();
        println!("tick {tick}: {}", states.join(" | "));
    }
}
Output
tick 1: player hp=100 x=1 | projectile x=3 range=1 | projectile x=8 range=3
tick 2: player hp=70 x=2 | projectile x=11 range=2
tick 3: player hp=70 x=3 | projectile x=14 range=1

26. Realistic example: storage backends behind one interface

Rust
trait BlobStore {
    fn put(&mut self, key: &str, data: &[u8]) -> Result<(), String>;
    fn get(&self, key: &str) -> Option<Vec<u8>>;
    fn describe(&self) -> String;
}

struct InMemory {
    blobs: Vec<(String, Vec<u8>)>,
}

struct SizeLimited {
    inner: Box<dyn BlobStore>,
    max_bytes: usize,
}

impl BlobStore for InMemory {
    fn put(&mut self, key: &str, data: &[u8]) -> Result<(), String> {
        self.blobs.retain(|(k, _)| k != key);
        self.blobs.push((key.to_string(), data.to_vec()));
        Ok(())
    }
    fn get(&self, key: &str) -> Option<Vec<u8>> {
        self.blobs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
    }
    fn describe(&self) -> String {
        format!("in-memory ({} blobs)", self.blobs.len())
    }
}

impl BlobStore for SizeLimited {
    fn put(&mut self, key: &str, data: &[u8]) -> Result<(), String> {
        if data.len() > self.max_bytes {
            return Err(format!("{key}: {} bytes exceeds limit of {}", data.len(), self.max_bytes));
        }
        self.inner.put(key, data)
    }
    fn get(&self, key: &str) -> Option<Vec<u8>> {
        self.inner.get(key)
    }
    fn describe(&self) -> String {
        format!("size-limited({}) over {}", self.max_bytes, self.inner.describe())
    }
}

fn main() {
    let mut store: Box<dyn BlobStore> = Box::new(SizeLimited { inner: Box::new(InMemory { blobs: Vec::new() }), max_bytes: 8 });
    println!("{:?}", store.put("avatar", b"PNG..."));
    println!("{:?}", store.put("video", b"way too large for the limit"));
    println!("{:?}", store.get("avatar").map(|b| String::from_utf8_lossy(&b).to_string()));
    println!("{}", store.describe());
}
Output
Ok(())
Err("video: 27 bytes exceeds limit of 8")
Some("PNG...")
size-limited(8) over in-memory (1 blobs)

SizeLimited wraps any other store, the decorator pattern, and is itself a BlobStore, so decorators can stack.

27. Realistic example: static and dynamic dispatch together

Generic code accepts trait objects too, because dyn Trait itself implements the trait. A single generic function then serves both fast, specialized calls and flexible runtime ones.

Rust
trait Metric {
    fn value(&self) -> f64;
}

struct Cpu(f64);
struct Memory { used: f64, total: f64 }

impl Metric for Cpu {
    fn value(&self) -> f64 {
        self.0
    }
}

impl Metric for Memory {
    fn value(&self) -> f64 {
        self.used / self.total * 100.0
    }
}

fn over_threshold<M: Metric + ?Sized>(metric: &M, threshold: f64) -> bool {
    metric.value() > threshold
}

fn main() {
    // static dispatch: M = Cpu
    println!("{}", over_threshold(&Cpu(91.0), 90.0));

    // dynamic dispatch: M = dyn Metric
    let metrics: Vec<Box<dyn Metric>> = vec![Box::new(Cpu(40.0)), Box::new(Memory { used: 15.0, total: 16.0 })];
    for m in &metrics {
        println!("{:.1} over 90: {}", m.value(), over_threshold(m.as_ref(), 90.0));
    }
}
Output
true
40.0 over 90: false
93.8 over 90: true

The ?Sized bound from Trait Bounds is what allows M = dyn Metric.

Summary

  • dyn Trait is "some type implementing Trait", resolved at runtime through a vtable. Use it behind a pointer: &dyn Trait, &mut dyn Trait, or Box<dyn Trait>.
  • Trait objects allow mixed collections (Vec<Box<dyn Trait>>), runtime choice of implementation, and returning different types from one function.
  • A trait object pointer is a fat pointer: data pointer plus vtable pointer.
  • Only dyn-compatible traits work as dyn Trait: no generic methods, no methods returning Self (unless marked where Self: Sized).
  • Clone boxed trait objects with a clone_box method. Downcast with Any only when truly needed.
  • Prefer enums for closed sets of types and trait objects for open ones. Prefer generics when you don't need mixing, for zero-cost static dispatch.

Next: Advanced Traits.