Using Trait Objects for Runtime Polymorphism
Trait objects enable storing different types in the same collection when they implement a common trait. They provide dynamic dispatch at runtime cost.
Basic Usage
Define a trait for common behavior:
pub trait Draw {
fn draw(&self);
}
Store trait objects using Box<dyn Trait>
:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Call methods on trait objects:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Trait Objects vs. Generics
Generics (monomorphization - static dispatch):
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
- All components must be the same type
- Zero-cost abstraction (compile-time dispatch)
- Code duplication for each concrete type
Trait Objects (dynamic dispatch):
- Mixed types in same collection
- Runtime cost for method lookup
- Single implementation in binary
Implementation Examples
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
Compile-time Safety
Rust enforces trait implementation at compile time:
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
Performance Trade-offs
Static Dispatch (generics):
- Faster execution (no runtime lookup)
- Larger binary size (code duplication)
- All types known at compile time
Dynamic Dispatch (trait objects):
- Runtime method lookup overhead
- Smaller binary size
- Enables heterogeneous collections
- Prevents some compiler optimizations
Choose based on your use case: use generics for performance-critical code with known types, trait objects for flexible APIs with mixed types.