2.9 Structs

In order for us to create more convoluted structures of data in Rust—we need to use the beauty that lies hidden behind structs. A struct is a way for us to create structures containing multiple elements of different types, that we can in turn create bindings from, in the form of a sort of blueprint.

Remember how we had fun earlier sinking battleships? Let’s go back to that trade, by rather than creating one variable for the x coordinate and one for the y, we will create a struct handling all that for us.

struct Coord
{
    x: i32, 
    y: i32
}

This struct contains two fields—x and y. These can then be accessed from outside the struct once we’ve created a binding from it. Rust's structs are quite similar to objects in other languages, but have a rather different logic as for its backend compared to other higehr-level languages.

The definition of a struct is placed outside all other functions, preferrably prior to.

We may now use the Coord struct from inside our functions—this in order to create more advanced structures of data:

fn main()
{
    let battleship_coord = Coord { x: 10, y: 7 };

    println!("There’s a battleship at ({}, {}). Sink it!", battleship_coord.x, battleship_coord.y);
}

Upon executing this program, we will be presented with the following output:

There’s a battleship at (10, 7). Sink it!

In the code example above, we’re required to provide the struct Coord with values corresponding to the blueprint of our struct. The way we’re providing its values is by using a ‘key: value’ type initialization. We can in turn access its different elements using a single period ., in order for us to retrieve one of the structs fields—for instance .x.

Rather than using snake case life_in_space like we do for functions and binding names, we use camel case LifeInSpace when naming structs.

Field level mutability

In some languages, you may be used to using field level mutability inside structs—meaning that different variables, or bindings in this case, inside the struct can be either mutable or immutable. This isn’t the case as for Rust. In Rust, the mutability of all the elements of a struct corresponds to the mutability of the let binding defining it.

let mut battleship_coord = Coord { x: 2, x: 23 };

battleship_coord.x += 12; // Works brilliantly!

Although this may seem quite strange at first, it also allows us to do this:

let mut battleship_coord = Coord { x: 2, x: 23};

battleship_coord.x += 12; // Works brilliantly!

let battleship_coord = battleship_coord; // No longer mutable!

battleship_coord.x += 12; // ARGH! All of a sudden I g3nerate an err0r!

This allows us to change the mutability of any struct at any given time.

Tuple structs

Tuple structs are hybrids between tuples and structs. The difference between a tuple struct and a regular struct is that the tuple structs are absent of field names—the types still need be there:

struct Coord(i32, i32); // Defined using parenthesis rather than curly braces

fn main()
{
    let c = Coord(2, 3);
}

Just like regular tuples you may access the different fields of a tuple struct either by . or by destructuring it using a let.

Usage

The tuple struct is useful—for instance, when creating some form of units and you don’t want it to go unnamed. Let’s pretend we’d like something called a Meter:

struct Meter(i32);

fn main()
{
    let wall_thickness = Meter(32);
}

All of a sudden, they’re all extremely useful! Now meters are actually of type Meter. Just imagine how wondeful it’d be to have 32-meter thick walls out of lead for example—the government would be completely unable to surveil our thoughts!

Unit-like structs

A struct can also be defined to contain no values—and could act as if a single unit:

struct Stamp {}

This may be expressed in an even nicer way:

struct Stamp;

Whether you choose to use curly braces upon defining your unit-like struct or not, you are required to do the same when binding a new one with let:

let stamp = Stamp {};

Or the nicer way:

let stamp = Stamp;

And from there we can make an array containing stamps—all of a sudden we can have a great deal of fun collecting stamps!

Methods

Any struct can also have methods inside of them—the same way classes in some other languages can. We may access the area where methods are stored using the keyword impl:

struct Village
{
    population: i32, 
    big_in_size: bool, 
    funny_people: bool
}

impl Village
{
    pub fn new() -> Village
    {
        Village {population: 55, big_in_size: false, funny_people: true}
    }
}

fn main()
{
    let village = Village::new();
}

In this example we're making use of a sort of constructor for the struct—as close to a constructor you can get in a language such as Rust. Whenever you wish to access a method from a struct without calling it on the actual struct, we access it using ::.

If, on the other hand, we wanted to access regular methods inside of Rust, this is what we’d do:

struct Beer
{
    brand: String
}

impl Beer
{
    fn print_brand(&self)
    {
        println!("The brand of beer is {}!", self.brand);
    }
}

fn main()
{
    let beer = Beer { brand: "Smithwick’s".to_string() };

    beer.print_brand();
}

In this example we’re calling a method inside a struct we’ve already defined into a let. The &self refers to ‘this’ instance of Beer. More on this in the Traits section.

Exercises

  • Create a struct for a person—storing information about their age, height and waist circumference. Make a constructor for the struct defining it into some default values. Also make a method inside the same struct printing out whether or not that person in particular is suited for running the marathon. Run the fitformarathon method.

Moreover

The usage of structs are a great means of structuring your program in an even more organized way—one might even say structs are required to turn your program into something useful. With a little help from structs, you can keep your program organized over multiple pieces—as well as create sort of ‘objects’ like you would in object-oriented programming languages. Since Rust isn’t an object-oriented programming language, it’s however not supposed to be structured in the exact same way. When structuring your Rust program, do your best to come up with non object-oriented solutions!