Chapter 11. A little example structs and traits
Now that we've added an overview of traits let's take a small detour and outline a small example using traits and structs together.
Our objective is to make a container, called Universe, that can basically hold anything.
So let's literally define an Anything trait and eventually store a Star in the Universe container.
The universe is not only queerer than we suppose, but queerer than we can suppose. - J.B.S. Haldane, geneticist and evolutionary biologist
Universe struct
Our Universe struct will have a single field, things that holds a dynamic number of objects.
I'll refer to the
Universestruct as a container - a term that simply means a user-defined place to store something. It's basically tupperware for software.
The struct essentially serves as a container that can hold a collection of diverse objects, unified by the requirement that they implement the Anything trait. On things it uses a Vec<Box<dyn Anything>> to store these objects, leveraging Rust’s dynamic dispatch to manage heterogeneity at runtime.
pub struct Universe {
things: Vec<Box<dyn Anything>>,
}
Adding some methods
Let's give our callers a simple API with things like Universe::new, add_things and list_things.
impl Universe {
pub fn new() -> Self {
Universe { things: Vec::new() }
}
pub fn add_things<T: Anything + 'static>(&mut self, thing: T) {
self.things.push(Box::new(thing));
}
pub fn list_things(&self) -> Vec<String> {
self.things.iter().map(|thing| thing.describe()).collect()
}
}
Next we'll add our trait which allows for objects of different types to be stored dynamically on the heap at runtime.
Anything trait
Our trait will have a single trait method describe that returns a string. This will require implementers to implement a describe method so we can do things like print out what is the type of the actual object.
pub trait Anything {
fn describe(&self) -> String;
}
Now our callers can store literally anything in the Universe things field, as long as they implement Anything.
Next we'll take a look at how exactly to do that.
Making a Star
First off we'll create a Star struct with a single field name.
struct Star {
name: String,
}
And let's now give it the standard new interface.
impl Star {
pub fn new(name: &str) -> Star {
Star {
name: name.to_string(),
}
}
}
Now let's implement the Anything trait for Star so we can store it in Universe.
impl Anything for Star {
fn describe(&self) -> String {
format!("{}", self.name)
}
}
Now we're ready to use all this. Let's create a Universe and add a Star!
fn main() {
let mut universe = Universe::new();
universe.add_things(Star::new("Olivia Rodrigo"));
for description in universe.list_things() {
println!("{}", description);
}
}
Wait, you didn't think we were going to make a stellar body, did you?
Recap
To show an example of structs and traits working together, we first created some structs which help us group our data together. A Universe holds things, such as a Star. We could also store a struct Mansion in Universe with different properties from Star, such as number_of_gardeners and cost_per_tennis_court.
Check out the tests within
lib.rsfor an example of multiple structs being stored.
We implemented the common trait Anything that makes sure our diverse types like Star and Mansion provide a describe method, so they can behave uniformly.
Using Box<dyn Anything> is used to enable dynamic dispatch, allowing our Universe collection to hold objects of different types, like Star and Anything, that implement the same trait.