Rust Trait Object Collections
Trait object collections hold values of different types in one collection through one shared trait. A Vec normally holds one type only.
Build the board
Use Vec<Box<dyn Trait>> to hold different concrete types that share one trait. Each value goes in with Box::new(...). The box owns the value. dyn says the concrete type is chosen at run time:
let mut board = Board::new();
board.add(Box::new(TextNotice { message: String::from("Museum opens at nine") }));
board.add(Box::new(Countdown { days: 5 }));Only trait methods are called
Callers use only the trait methods: show never learns which concrete type sits in each box:
board.show();The board is a notice too
A struct that owns trait objects can implement the trait itself — the composite pattern. Board answers text() by joining the text of everything it holds:
impl Notice for Board {
fn text(&self) -> String {
self.notices.iter().map(|n| n.text()).collect::<Vec<_>>().join(" | ")
}
}Boards can hold boards
Because Board is itself a Notice, the summary call works on the whole board at once:
println!("summary: {}", board.text());Example
trait Notice {
fn text(&self) -> String;
}
struct TextNotice {
message: String,
}
impl Notice for TextNotice {
fn text(&self) -> String {
self.message.clone()
}
}
struct Countdown {
days: u32,
}
impl Notice for Countdown {
fn text(&self) -> String {
format!("{} days left", self.days)
}
}
struct Board {
notices: Vec<Box<dyn Notice>>,
}
impl Board {
fn new() -> Board {
Board { notices: Vec::new() }
}
fn add(&mut self, notice: Box<dyn Notice>) {
self.notices.push(notice);
}
fn show(&self) {
for notice in &self.notices {
println!("- {}", notice.text());
}
}
}
impl Notice for Board {
fn text(&self) -> String {
self.notices.iter().map(|n| n.text()).collect::<Vec<_>>().join(" | ")
}
}
fn main() {
let mut board = Board::new();
board.add(Box::new(TextNotice { message: String::from("Library closes early today") }));
board.add(Box::new(Countdown { days: 3 }));
board.show();
println!("summary: {}", board.text());
}Key points
- Adding a new kind of item needs no change to the collection.
- Write a new type.
- Implement the trait.
- Push it in
- A window holding buttons and labels as Box<dyn Widget> is the same shape as this board.
- One collection can hold different types that share a trait.
In this example, Notice is a trait with one method, text. TextNotice and Countdown are different types that both implement it. Board owns a Vec<Box<dyn Notice>>, so add can accept either type, each wrapped in a Box. show walks the Vec and prints each notice's text — it never asks which concrete type it is holding. The last line shows the composite pattern. Board itself implements Notice by joining its children's texts. So a board could be added to another board, just like any other notice.
Note
This is the pattern behind plugin systems and widget trees.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why must each value be wrapped in Box before it goes into a Vec<Box<dyn Notice>>?