Structs

Structuring your data is a key part of programming, and every language worth its salt gives you a way to do that. In object-oriented languages, this is usually by creating a class. In Rust, you structure your data with structs.

A struct is a named grouping of fields (data belonging to the struct), which also can have methods on it. Let's unpack that.

When you're creating a struct, first you have to give it a name:

#![allow(unused)]
fn main() {
struct PirateShip {
}
}

Then you can also put fields on it, of any type:

#![allow(unused)]
fn main() {
struct PirateShip {
    captain: String,
    crew: Vec<String>,
    treasure: f64,
}
}

And you can have methods on the struct by using an impl block. Those methods can take a reference to self (&self) if they are just reading fields, or they can use a mutable reference (&mut self) if they will be changing any data.

#![allow(unused)]
fn main() {
struct PirateShip {
   captain: String,
   crew: Vec<String>,
   treasure: f64,
}
impl PirateShip {
    pub fn count_treasure(&self) -> f64 {
        // some computations probably
        self.treasure
    }

    pub fn mutiny(&mut self) {
        if self.crew.len() > 0 {
            // replace the captain with one of the crew
            self.captain = self.crew.pop().unwrap();
        } else {
            println!("there's no crew to perform mutiny");
        }
    }
}
}

To create an instance of a struct, you give the name of the struct along with a value for each of the fields, specified by name (like treasure: 64.0). There is also some shorthand to use: if you have a variable in scope with the same name as one of the fields, you can specify that just by name. That's confusing without an example, so let's see it in action.

#![allow(unused)]
fn main() {
struct PirateShip {
   captain: String,
   crew: Vec<String>,
   treasure: f64,
}
let blackbeard = "Blackbeard".to_owned();
let crew = vec!["Scurvy".to_owned(), "Rat".to_owned(), "Polly".to_owned()];
let ship = PirateShip {
    captain: blackbeard,
    crew,
    treasure: 64.0,
};
}

In this example, we can see both forms of specifying fields. The captain and treasure are specified with the <name>: <value> form, while the crew is specified with the shorthand that means crew: crew.

Note that in this example, we used the method to_owned a few times. This takes a reference to a string (&str) and creates an owned string (String), so that we don't have to worry about lifetimes. The precise details of this aren't particularly relevant in this chapter, but it's a nice thing to keep in mind: if you want to avoid including lifetimes, you can use owned instances by cloning (or a method like to_owned). There's more complexity with strings in Rust than in other languages due to references and lifetimes, but further treatment of them is beyond the scope of this course.

Exercises:

  1. Define a struct for a crew member with a name, age, and any other attributes you would like.
  2. Implement a few methods on this struct, such as one to say who it is.