In the previous chapter, we saw we can use slices as references to a contiguous sequence of elements in a collection instead of the whole collection. In this chapter, we will deviate from Ownership rules and see how we can use structs to create custom data types.

Structs

In Rust, and in object-oriented programming languages, a Struct or structure is a custom data type that lets you hold multiple values in relation to it. It is like a tuple in the sense that a tuple also holds multiple values, but a tuple does not have names associated with the values. A Struct is similar to a tuple in the sense that it also holds multiple values, but it differs in the sense that it has names associated with the values.

A struct is defined as follows:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

and is initialized as follows:

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

Note: Syntax: When initializing a struct, the order of fields does not matter. We need curly brackets containing key-value pairs for each of the field of a struct.

We can extract values from a struct using the . operator followed by the key.

Note: Design Choice: If we want to modify a field of a struct, the entire struct has to be mutable, rather than a single field.

Structs can also be return values of functions, as shown below:

struct User { // defining a struct
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}


fn build_user(email: String, username: String) -> User {
    User { // returning an initialized struct
        email: email, // we can use shorthand syntax and just put 'email' instead of 'email: email'
        username: username, // can also use shorthand syntax here
        active: true,
        sign_in_count: 1,
    }
}

Creating a new struct by reusing values from an old struct

The struct update syntax helps us reuse most of the values from a struct to create a new struct. This is useful when only have to modify a few values in the new struct.

Suppose we have a user1 object from the User struct as follows:

let user1 = User {
    active: true,
    username: String::from("someusername123"),
    email: String::from("someone@example.com"),
    sign_in_count: 1,
};

Let’s say we want to create another user that only has a different email, but the rest of the values are the same as that of user1. This can be done as follows:

let user2 = User {
    email: String::from("user2email@example.com"),
    active: user1.active, // we can just get the value from user1 object
    username: user1.username, // we can just get the value from user1 object
    sign_in_count: user1.sign_in_count, // we can just get the value from user1 object
};

An even better shorthand for this is as follows:

let user2 = User{
    email: String::from("user2email@example.com"),
    ..user1 // take values for the rest of the fields as defined in 'user1'
}

Ownership of Struct Data

In the above example, when we create user2, we will no longer have access to user1 object. This is because of the = operator. When we use the = operator, the value is moved from the old variable to the new field, and the field username of user1, which is a String, will transfer ownership to user2.

On the other hand, if both email and username fields are changed for user2, then user1 will still be valid, as the ownership of the String values will not be transferred to user2, and active and sign_in_count are both stored on Stack as their sizes are known at compile time. Boom!

Tuple Structs

We can also define structs that look like tuples, but are different types. These are called tuple structs. They have no key names associated with the values, but they are different types. For example:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

are two different tuples of type Color and Point respectively, even though they have the same number of values and the same types of values. A function that takes Color type as an argument cannot take a Point type as an argument, even though they look the same.

How and when to use Structs

Using tuple can be useful in some scenarios where we want to link values to each other. For example, if we want to calculate the area of a rectangle, we could use a tuple as follows:

fn main() {
    let rectangle = (10, 20); // tuple containing width and height
    
    compute_area(rectangle); // pass a tuple struct as an argument  
    
    fn compute_area(dims: (i32, i32)) -> i32 {
        dims.0 * dims.1
    }
}

The above implementation uses a tuple struct Rectangle, and we pass the width and the height to it together as a tuple. But what if we wanted to plot the rectangle? We would then need to keep track of which index represents the width and height. That can get messy very fast.

Using struct instead

Instead of passing a tuple, we can improve the readiblity of the code above by creating a struct Rectangle and assigning the width and height to it as follows:

fn main() {
    struct Rectangle {
        width: i32,
        height: i32,
    }

    let rect = Rectangle {
        width: 10,
        height: 20
    };

    compute_area(&rect);

    fn compute_area(rect: &Rectangle) {
        let rect_area = rect.width * rect.height;

        println!("The area of the rectangle is {rect_area}")
    }
}

We can see that the implementation that we have now is much easier to understand since the paremeters width and height are linked to the struct Rectangle.

Adding more functionality to structs

So far, we have not tried to print a struct object in rust. Let’s try the following to print a struct object:

fn main() {
    struct Rectangle {
        width: i32,
        height: i32,
    }
    ;

    let rect = Rectangle {
        width: 10,
        height: 20
    };

    println!("The rectangle is {rect}", rect = rect);
}

When run, we should get the following error:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
  --> src/main.rs:15:60
   |
15 |     println!("The area of the rectangle is {rect}", rect = rect);
   |                                                            ^^^^ `Rectangle` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

Okay, compiler says let’s try to print with {:?} instead. Let’s try that:

use std::io;

fn main() {
    struct Rectangle {
        width: i32,
        height: i32,
    }


    let rect = Rectangle {
        width: 10,
        height: 20
    };

    println!("The rectangle is {:?}", rect);
}

When run, now we get another error:

error[E0277]: `Rectangle` doesn't implement `Debug`
  --> src/main.rs:15:51
   |
15 |     println!("The area of the rectangle is {:?}", rect);
   |                                                   ^^^^ `Rectangle` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Rectangle` with `#[derive(Debug)]`
   |
4  |     #[derive(Debug)]
   |

The error now says Rectangle doesn't implement Debug.

In rust, Debug is a trait that allows us to print the data of a variable such that it is easier for us to debug. The compiler also gives us help: add '#[derive(Debug)]' to Rectangle or manually 'impl Debug for Rectangle'

To resolve this issue, we can modify our code as following:

fn main() {
    #[derive(Debug)] // we added the Debug trait to our struct
    struct Rectangle {
        width: i32,
        height: i32,
    }

    let rect = Rectangle {
        width: 10,
        height: 20,
    };

    println!("The rectangle is {:?}", rect); // this should now work and show the data in the struct
}

Run the above code and be in for a surprise!

Attaching a method to a struct

The function to compute area is very specific to rectangles, and will not work for circles or triangles. In that, we should link the compute_area function to the Rectangle struct. This would be attaching a method to the struct. Let’s see how we can do that:

fn main() {
    struct Rectangle {
        // create struct Rectangle
        width: i32,
        height: i32,
    }

    let rect = Rectangle { // create an instance of Rectangle
        width: 10,
        height: 20,
    };

    impl Rectangle { // start implementation block
        fn area(&self) -> i32 { // associate method to Rectangle struct via &self
            self.width * self.height
        }
    }

    println!("The area of the rectangle is {}", rect.area());
}

We see that using the keyword impl and then the name of the struct, we are able to create a method for that struct. For the method that we create, we need to first argument to be the self keyword, so that the method knows that it is associated with the struct. We can then use the self keyword to access the values of the struct.

Important Note: In rust, instead of using the self keyword directly, we use the &self keyword to avoid taking ownership of the struct. The documentation also mentions that it is very rare that a method takes ownership of the struct, and most of the time we just use the reference of the struct as input via &self.

In rust, we can also set getters for attributes by creating methods for them. Getters are not set by default in rust, unlike in Python or other languages. When creating a getter method, we usually set the name of the method equal to the name of the attribute.

Methods with more Parameters

We can keep multiple parameters in the method, and use them as we would in any other function. We can also link multiple methods to a struct.