We create a trait object by specifying some sort of pointer, such as a &
reference or a Box<T>
smart pointer, then the dyn
keyword, and then specifying the relevant trait.
// the trait we need for Screen's container
pub trait Draw {
fn draw(&self);
}
// the upper level abstraction for some purpose
pub struct Screen {
pub components: Vec<Box<dyn Draw>>, // elements in Vec can be different types.
}
// implement Screen to run some drawings
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
// implement one possible container element type
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
}
}
// implement another
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() {
let screen = Screen {
components: vec![
Box::new(SelectBox { // now we can use SelectBox in the components Vec
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button { // now we can use Button
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
This works differently from defining a struct that uses a generic type parameter with trait bounds. A generic type parameter can only be substituted with one concrete type at a time, whereas trait objects allow for multiple concrete types to fill in for the trait object at runtime.
Monomorphization process will be performed by the compiler when we use trait bounds on generics. The code that results from monomorphization is doing static dispatch, which is when the compiler knows what method you’re calling at compile time. This is opposed to dynamic dispatch , which is when the compiler can’t tell at compile time which method you’re calling. In dynamic dispatch cases, the compiler emits code that at runtime will figure out which method to call.