Rust - Trait Objects

Overview

Estimated time: 40–50 minutes

Trait objects allow for dynamic dispatch and runtime polymorphism in Rust. Learn about the dyn keyword, object safety rules, and when to choose trait objects over generics for flexible code design.

Learning Objectives

Prerequisites

What Are Trait Objects?

Trait objects allow you to store values of different types that implement the same trait. Unlike generics (static dispatch), trait objects use dynamic dispatch - the exact method to call is determined at runtime.

trait Draw {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

struct Square {
    side: f64,
}

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

impl Draw for Square {
    fn draw(&self) {
        println!("Drawing a square with side {}", self.side);
    }
}

fn main() {
    // Using trait objects with dyn keyword
    let shapes: Vec<Box<dyn Draw>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Square { side: 3.0 }),
    ];
    
    for shape in shapes {
        shape.draw();  // Dynamic dispatch
    }
}

Expected Output:

Drawing a circle with radius 5
Drawing a square with side 3

The dyn Keyword

The dyn keyword explicitly marks trait objects, making the code clearer:

// Modern syntax (Rust 2018+)
let drawable: &dyn Draw = &Circle { radius: 2.0 };
let drawable_box: Box<dyn Draw> = Box::new(Square { side: 4.0 });

// You can also use Rc or Arc for shared ownership
use std::rc::Rc;
use std::sync::Arc;

let shared_drawable: Rc<dyn Draw> = Rc::new(Circle { radius: 1.0 });
let thread_safe_drawable: Arc<dyn Draw> = Arc::new(Square { side: 2.0 });

Object Safety Rules

Not all traits can be made into trait objects. A trait is "object-safe" if:

  1. The trait doesn't require Self: Sized
  2. All methods are object-safe

Object-Safe Methods

A method is object-safe if:

Object-Safe Trait Example

trait Printable {
    fn print(&self);  // Object-safe
    fn name(&self) -> &str;  // Object-safe
}

struct Document { content: String }
struct Image { pixels: Vec<u8> }

impl Printable for Document {
    fn print(&self) {
        println!("Printing document: {}", self.content);
    }
    
    fn name(&self) -> &str {
        "Document"
    }
}

impl Printable for Image {
    fn print(&self) {
        println!("Printing image with {} pixels", self.pixels.len());
    }
    
    fn name(&self) -> &str {
        "Image"
    }
}

fn print_item(item: &dyn Printable) {
    println!("Item type: {}", item.name());
    item.print();
}

fn main() {
    let doc = Document { content: "Hello World".to_string() };
    let img = Image { pixels: vec![0; 1000] };
    
    print_item(&doc);
    print_item(&img);
}

Expected Output:

Item type: Document
Printing document: Hello World
Item type: Image
Printing image with 1000 pixels

Non-Object-Safe Trait Example

trait Clone {
    fn clone(&self) -> Self;  // Returns Self - not object-safe
}

trait Collect<T> {
    fn collect(&self) -> T;  // Generic method - not object-safe
}

// This won't compile:
// let cloneable: &dyn Clone = &5;  // Error!

Practical Example: Plugin System

trait Plugin {
    fn name(&self) -> &str;
    fn execute(&self, input: &str) -> String;
}

struct UppercasePlugin;
struct ReversePlugin;
struct CountPlugin;

impl Plugin for UppercasePlugin {
    fn name(&self) -> &str {
        "Uppercase"
    }
    
    fn execute(&self, input: &str) -> String {
        input.to_uppercase()
    }
}

impl Plugin for ReversePlugin {
    fn name(&self) -> &str {
        "Reverse"
    }
    
    fn execute(&self, input: &str) -> String {
        input.chars().rev().collect()
    }
}

impl Plugin for CountPlugin {
    fn name(&self) -> &str {
        "Character Count"
    }
    
    fn execute(&self, input: &str) -> String {
        format!("Length: {}", input.len())
    }
}

struct PluginManager {
    plugins: Vec<Box<dyn Plugin>>,
}

impl PluginManager {
    fn new() -> Self {
        PluginManager {
            plugins: Vec::new(),
        }
    }
    
    fn add_plugin(&mut self, plugin: Box<dyn Plugin>) {
        self.plugins.push(plugin);
    }
    
    fn execute_all(&self, input: &str) {
        for plugin in &self.plugins {
            let result = plugin.execute(input);
            println!("{}: {}", plugin.name(), result);
        }
    }
}

fn main() {
    let mut manager = PluginManager::new();
    
    manager.add_plugin(Box::new(UppercasePlugin));
    manager.add_plugin(Box::new(ReversePlugin));
    manager.add_plugin(Box::new(CountPlugin));
    
    manager.execute_all("Hello Rust");
}

Expected Output:

Uppercase: HELLO RUST
Reverse: tsuR olleH
Character Count: Length: 10

Trait Objects with Associated Types

Traits with associated types can be object-safe if the associated type is specified:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// This won't work - associated type not specified
// let iter: &mut dyn Iterator = &mut vec![1, 2, 3].into_iter();

// This works - associated type specified
let mut numbers = vec![1, 2, 3].into_iter();
let iter: &mut dyn Iterator<Item = i32> = &mut numbers;

println!("Next: {:?}", iter.next());

Performance Considerations

Static vs Dynamic Dispatch

// Static dispatch (generics) - faster, larger code
fn process_static<T: Draw>(drawable: T) {
    drawable.draw();  // Compiler knows exact type
}

// Dynamic dispatch (trait objects) - slower, smaller code
fn process_dynamic(drawable: &dyn Draw) {
    drawable.draw();  // Runtime lookup required
}

fn main() {
    let circle = Circle { radius: 1.0 };
    
    // Static dispatch - no runtime overhead
    process_static(circle);
    
    // Dynamic dispatch - small runtime overhead
    process_dynamic(&Circle { radius: 2.0 });
}

Advanced Example: Event System

trait EventHandler {
    fn handle(&self, event: &str);
}

struct Logger;
struct Notifier { recipient: String }
struct Analyzer;

impl EventHandler for Logger {
    fn handle(&self, event: &str) {
        println!("[LOG] {}", event);
    }
}

impl EventHandler for Notifier {
    fn handle(&self, event: &str) {
        println!("[NOTIFY {}] {}", self.recipient, event);
    }
}

impl EventHandler for Analyzer {
    fn handle(&self, event: &str) {
        println!("[ANALYZE] Processing: {} (length: {})", event, event.len());
    }
}

struct EventBus {
    handlers: Vec<Box<dyn EventHandler>>,
}

impl EventBus {
    fn new() -> Self {
        EventBus { handlers: Vec::new() }
    }
    
    fn subscribe(&mut self, handler: Box<dyn EventHandler>) {
        self.handlers.push(handler);
    }
    
    fn publish(&self, event: &str) {
        for handler in &self.handlers {
            handler.handle(event);
        }
    }
}

fn main() {
    let mut bus = EventBus::new();
    
    bus.subscribe(Box::new(Logger));
    bus.subscribe(Box::new(Notifier { 
        recipient: "admin".to_string() 
    }));
    bus.subscribe(Box::new(Analyzer));
    
    bus.publish("User logged in");
    println!();
    bus.publish("File uploaded");
}

Expected Output:

[LOG] User logged in
[NOTIFY admin] User logged in
[ANALYZE] Processing: User logged in (length: 14)

[LOG] File uploaded
[NOTIFY admin] File uploaded
[ANALYZE] Processing: File uploaded (length: 13)

When to Use Trait Objects vs Generics

Use Trait Objects When:

Use Generics When:

Common Pitfalls

1. Forgetting the dyn Keyword

// Old style (deprecated)
// let drawable: Box<Draw> = Box::new(Circle { radius: 1.0 });

// Correct modern style
let drawable: Box<dyn Draw> = Box::new(Circle { radius: 1.0 });

2. Trying to Use Non-Object-Safe Traits

trait BadTrait {
    fn bad_method(&self) -> Self;  // Not object-safe
}

// This won't compile:
// let obj: &dyn BadTrait = &some_value;

3. Size Issues with Trait Objects

// This won't work - trait objects are unsized
// let drawable: dyn Draw = Circle { radius: 1.0 };

// These work - trait objects behind a pointer
let drawable: &dyn Draw = &Circle { radius: 1.0 };
let drawable: Box<dyn Draw> = Box::new(Circle { radius: 1.0 });

Summary

Trait objects provide runtime polymorphism in Rust:

Choose trait objects when you need runtime polymorphism and can accept the small performance overhead for the flexibility they provide.


← PreviousNext →