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.

Chapter 11. A little example structs and traits

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 Universe struct 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.rs for 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.

Was this page helpful?