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
- Understand what trait objects are and how they enable dynamic dispatch.
- Learn the object safety rules and which traits can be made into objects.
- Master the
dyn
keyword and trait object syntax. - Compare trait objects vs generics for different use cases.
- See practical examples with collections of different types.
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:
- The trait doesn't require
Self: Sized
- All methods are object-safe
Object-Safe Methods
A method is object-safe if:
- It doesn't have generic type parameters
- It doesn't return
Self
- It doesn't have
Self
as a parameter except as a receiver (&self
,&mut self
,self
)
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:
- You need to store different types in the same collection
- The exact types are unknown at compile time
- You want to reduce code size (avoid monomorphization)
- Building plugin systems or event handlers
- Runtime polymorphism is required
Use Generics When:
- Performance is critical (avoid virtual calls)
- You know the types at compile time
- You need to return
Self
or use generic methods - The trait is not object-safe
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:
- Dynamic dispatch: Method calls are resolved at runtime
- dyn keyword: Explicitly marks trait objects for clarity
- Object safety: Not all traits can become trait objects
- Flexibility: Store different types implementing the same trait
- Trade-offs: Slight runtime cost for increased flexibility
- Use cases: Plugin systems, event handlers, heterogeneous collections
Choose trait objects when you need runtime polymorphism and can accept the small performance overhead for the flexibility they provide.