Rust Structs (Structures) Tutorial

In this Rust tutorial we learn how to create our own custom data types, with structs, that groups variables and functions together into reusable units and help us model our application like real world entities.

We cover how to define and initialize a struct with values, access and mutate its properties and methods, as well as how to structs interact with functions.

What is a struct (structure)

A struct is a user-defined type that we can use to store one or more variables of different types. A struct allows us to group related code together and model our application after entities in the real world.

As an example, let’s consider a player character in a game. Each player has a list of properties associated with them.

  • Player name
  • Hit points
  • etc.

Our first thought would be store these properties into an array, but the properties have different types, so we can’t.

This is where a struct comes in handy. We can group these properties together as a single custom unit, and from it create different units.

For example, we group all the properties associated with a player into a struct called Player. Then, we create multiple players of type Player, each one with different values.

How to define a struct

We define a struct with the struct keyword, followed by a unique name and a code block. Inside the code block we specify one or more properties, with their data types, separated by a comma.

Syntax: define a struct
struct struct_name {

    property_name: data_type,
    property_name: data_type,
    property_name: data_type

}
Example: define a struct
fn main() {}

struct Player {

    name: String,
    hit_points: u32,

}

In the example above, we create a struct called Player with two properties of different types.

The definition of a struct only defines what it will look like, it’s not initialized with values in the definition.

How to initialize a struct

Once we’ve defined a struct, we can initialize an instance of it with the specific values we want.

Syntax: initialize a struct instance with values
// definition
struct struct_name {

    property_name: data_type,
    property_name: data_type,
    property_name: data_type

}

// initialization
let variable_name = struct_name {

    property_name: property_value,
    property_name: property_value,
    property_name: property_value

};

There are two inportant factors to note when initializing the struct.

  1. Values are not assigned to properties with the = operator, but instead with the colon operator. This is because struct properties are key:value pairs.
  2. The struct initialization statement is terminated with a semicolon.
Example: initialize a struct instance with values
fn main() {

    // init structs
    let player_1 = Player {
        name: String::from("Mario"),
        hit_points: 100
    };

    let player_2 = Player {
        name: String::from("Luigi"),
        hit_points: 80
    };

}

// define struct
struct Player {
    name: String,
    hit_points: u32,
}

In the example above, we initialize two structs player_1 and player_2 with different values.

It’s the same as if we created two different tuples. But, our struct has a custom structure that another developer will have to follow.

How to access struct properties (members)

We access struct properties with dot notation. That means we separate the struct’s variable name and its property name with the dot operator.

Syntax: access struct members
// initialization
let variable_name = struct_name {

    property_name: property_value,
    property_name: property_value,
    property_name: property_value

};

// access
variable_name.property_name;
Example: access struct members
fn main() {

    // init structs
    let player_1 = Player {
        name: String::from("Mario"),
        hit_points: 100
    };

    let player_2 = Player {
        name: String::from("Luigi"),
        hit_points: 80
    };

    // access structs
    println!("Player 1: {}  HP: {}", player_1.name, player_1.hit_points);
    println!("Player 2: {}  HP: {}", player_2.name, player_2.hit_points);

}

// define struct
struct Player {
    name: String,
    hit_points: u32,
}

In the example above, we print the properties from each struct by accessing their values with dot notation.

How to change struct property values

To be able to change struct properties we have to mark each struct instance as mutable with the mut keyword.

Example: mutate struct property values
fn main() {

    // init structs
    let player_1 = Player {
        name: String::from("Mario"),
        hit_points: 100
    };

    // this struct instance is mutable
    let mut player_2 = Player {
        name: String::from("Luigi"),
        hit_points: 80
    };

    // access structs
    println!("Player 1: {}  HP: {}", player_1.name, player_1.hit_points);
    println!("Player 2: {}  HP: {}", player_2.name, player_2.hit_points);

    // mutate a specific struct instance's values
    player_2.hit_points = 100;
    println!("\nPlayer 2: {}  HP: {}", player_2.name, player_2.hit_points);

}

// define struct
struct Player {
    name: String,
    hit_points: u32,
}

In the example above, the player_2 instance of the struct is marked as mutable. We can then change the values of its properties.

How to pass a struct to a function

We can pass a struct instance to a function. To do this we specify the parameter name, followed by a colon operator and the struct we intend to pass.

Syntax: pass a struct to a function
// struct definition
struct struct_name {

    property_name: data_type,
    property_name: data_type,
    property_name: data_type

}

fn function_name(param_name:struct_name) {

    // do something with param_name.property_name

}
Example: pass a struct to a function
fn main() {

    // init structs
    let player_1 = Player {
        name: String::from("Mario"),
        hit_points: 100
    };
    let player_2 = Player {
        name: String::from("Luigi"),
        hit_points: 80
    };

    // pass struct instances
    // to function as parameter
    log(player_1);
    log(player_2);

}

// define struct
struct Player {
    name: String,
    hit_points: u32,
}

// function that accepts
// a Player struct instance
fn log(player:Player) {

    println!("Player: {}  HP: {}", player.name, player.hit_points);

}

In the example above, we moved our println inside the function log with a parameter called player that only accepts instances of the Player struct. Then, we call the function twice with both initialized struct names as parameters.

How to return a struct from a function

To return a struct from a function, we specify the function’s return type as the struct name.

Syntax: return struct from a function
// struct definition
struct struct_name {

    property_name: data_type,
    property_name: data_type,
    property_name: data_type

}

fn function_name() -> struct_name {

    // do something

    return struct_name;

}

As an example, let’s consider a function that returns the player with more hitpoints.

Example: return struct from a function
fn main() {

    // init structs
    let player_1 = Player {
        name: String::from("Mario"),
        hit_points: 100
    };
    let player_2 = Player {
        name: String::from("Luigi"),
        hit_points: 80
    };

    // call function and return value
    let healthiest = healthiest(player_1, player_2);

    // use returned value
    println!("Healthiest player: {}", healthiest.name);

}

// define struct
struct Player {
    name: String,
    hit_points: u32,
}

// return the player that has more hit points
fn healthiest(p1:Player, p2:Player) -> Player {

    if p1.hit_points > p2.hit_points {
        return p1;
    } else {
        return p2;
    }

}

In the example above, our function returns the player that has the most hit points between the two of them.

We then call the function and store its output in a variable. From the variable we will be able to access the properties of whichever struct instance has the most hit points.

How to define a function (method) specific to a struct

We can define functions that directly correspond to a struct. These functions are commonly known as methods and are only available to an instance of that particular struct.

To define a method, we use the impl keyword.

Syntax: define struct specific method
// struct definition
struct struct_name {

    property_name: data_type

}

// struct specific functions
impl struct_name {

    fn method_name() {
        // method body
    }

}
Example: define struct specific method
fn main() {

    // init structs
    let player_1 = Player {
        name: String::from("Mario")
    };
    let player_2 = Player {
        name: String::from("Luigi")
    };

    // use method
    player_1.log_name();
    player_2.log_name();

}

// define struct
struct Player {
    name: String
}

// Player specific methods
impl Player {

    fn log_name(self) {
        println!("Name: {}", self.name);
    }

}

We use impl Player to define methods specific only to the Player struct. In this case we define a simple method that prints the name of the player to the console.

However, the method has a strange parameter, self. The self parameter allows us to refer to the calling instance, meaning the instance of the struct that is using the method.

So, if we use the method with the player_1 instance, it would be similar to writing log_name(player_1) if it was a normal function.

Summary: Points to remember

  • A struct is a uder-defined type that allows us to group together functions, as well as variables of different types. This helps us to model our application after entities in the real world.
    • An example would be a car that has the properties Make and Model and the functionaly to drive().
  • A struct is defined with the struct keyword and property names and data types in key:value syntax.
  • An instance of a struct is initilialized by assigning it to a variable and specifying values for each property, also in key:value syntax.
  • A struct instance stands on its own, its member values are independent from those in other instances.
  • Once an instance of a struct exists, we can access its members with dot notation.
  • To enable members to be changed, we must mark the individual struct instance as mutable with the mut keyword.
  • We can pass a struct to a function by specifying the struct name as the type in the parameter list.
  • We can return a struct from a function by specifying the struct name as the return type.
  • We can define functions that are specific to a struct, called methods, that can only be used by instances of that struct.
    • A method must be defined inside a impl struct_name code block.
    • The self keyword is used to refer to the calling instance.