Fun Guides: Structs

A fun but comprehensive guide for Structs in Rust with metaphors from The Game of Thrones.

The Basics of Structs

When we want to group related values, structs come really handy.

For example, we can define each great house in the Game of Thrones with some specific attributes, such as name and region.

struct House {
  name: String,
  region: String,
}

fn main() {
  let house_stark = House {
    name: String::from("Stark"),
    region: String::from("The North"),
  };

  println!("House {} of {}", house_stark.name, house_stark.region);
}

We defined a noble house in Westeros as a House struct with fields for name and region.

Methods in Structs

Each house has some specific behavior. We can define those with methods.

Let's say that each house can rally its bannermen for aid. We will implement this action as a method of the House struct while adding the number of bannerman as another field in the House struct.

struct House {
  name: String,
  region: String,
  bannermen: u32,
}

impl House {
  fn rally_bannermen(&mut self, additional: u32) {
    self.bannermen += additional;
    println!("House {} now has {} bannermen.", self.name, self.bannermen);
  }
}

fn main() {
  let mut house_arryn = House {
    name: String::from("Arryn"),
    region: String::from("The Vale"),
    bannermen: 500,
  };

  house_arryn.rally_bannermen(300);
}

In the example above, the House Arryn can use the rally_bannermen method to increase the number of its bannermen.

Associated Functions

Associated functions represent the cold face of a house shown to strangers. They are related to a struct but don't use information of the struct and neither update the struct.

Creating a new house in Westeros is a significant event and can be represented as an associated function. Notice that the found_new_house function has neither the &self nor &mut self parameter - so it's not a method but just an associated function.

struct House {
  name: String,
  region: String,
  bannermen: u32,
}

impl House {
  fn found_new_house(name: String, region: String) -> House {
    House {
      name,
      region,
      bannermen: 100, // Default number of bannermen for a new house
    }
  }
}

fn main() {
  let house_tarly = House::found_new_house(
    String::from("Tarly"),
    String::from("The Reach"),
  );
  println!("House {} of {} has been founded.", house_tarly.name, house_tarly.region);
}

The function found_new_house is associated with the House struct now. It creates a new house with a default bannermen number, but it neither reads any info from a House and neither updates an existing House.

Core differences between struct methods and associated functions:

  • Associated functions don't receive the &self / &mut self argument, so they don't have access to the struct's properties, but struct methods do.
  • Associated functions are called with the :: notation as opposed to struct methods which are called with the . notation.

Structs with Enums

Just as houses have complex structures, so too can Rust structs incorporate enums for added flexibility. Enums in Rust allow us to define a type by enumerating its possible variants. This will be useful when dealing with properties that can have one out of a set of possible values.

Let's introduce an enum to represent the current state of a house in Westeros, such as at peace or war. This enum, named HouseState, can then be incorporated into our House struct.

#[derive(Debug)]
enum HouseState {
  AtPeace,
  AtWar,
}

struct House {
  name: String,
  region: String,
  bannermen: u32,
  state: HouseState,
}

impl House {
  fn change_state(&mut self, new_state: HouseState) {
      self.state = new_state;
  }
}

fn main() {
  let mut house_lannister = House {
      name: String::from("Lannister"),
      region: String::from("The Westerlands"),
      bannermen: 1000,
      state: HouseState::AtPeace,
  };

  house_lannister.change_state(HouseState::AtWar);

  println!("House {} is now {:?}", house_lannister.name, house_lannister.state);
}

The first line, #[derive(Debug)], is there just so that Rust knows how to print the enum value with println! at the end of the main function.

The important thing here is that we defined the HouseState enum with two variants. Each House struct now also includes a state field of type HouseState. The method change_state allows a house to change its state, reflecting dynamic political scenarios in Westeros.

Structs with Traits

Just as each house has its unique traits, we can define traits to encapsulate shared behaviors across different Rust structs. Traits in Rust are similar to interfaces in other languages; they define a set of methods that can be implemented by structs.

Let's define a trait HouseTraits that represents common actions of a noble house, such as hosting a feast or defending its lands. This trait can then be implemented by our House struct.

#[derive(Debug)]
enum HouseState {
  AtPeace,
  AtWar,
}

struct House {
  name: String,
  region: String,
  bannermen: u32,
  state: HouseState,
}

trait HouseTraits {
  fn host_feast(&self);
  fn defend_lands(&mut self, additional_bannermen: u32);
}

impl HouseTraits for House {
  fn host_feast(&self) {
      println!("House {} is hosting a grand feast!", self.name);
  }

  fn defend_lands(&mut self, additional_bannermen: u32) {
      self.bannermen += additional_bannermen;
      println!("House {} defends its lands with {} bannermen.", self.name, self.bannermen);
  }
}

fn main() {
  let mut house_baratheon = House {
      name: String::from("Baratheon"),
      region: String::from("The Stormlands"),
      bannermen: 800,
      state: HouseState::AtWar,
  };

  house_baratheon.host_feast();
  house_baratheon.defend_lands(200);
}

In this example, the HouseTraits trait includes two methods: host_feast, which is a read-only action, and defend_lands, which modifies the state of the struct. The House struct then implements these methods, providing specific behaviors.

Core differences between struct methods and traits:

  • struct methods are specific to a particular struct,
  • traits define a set of methods that can be implemented by multiple types (including structs)