Skip to content

Using Structs to Structure Related Data

A struct is a custom data type. It is like an object's data attributes if you're thinking about object-oriented programming.

Defining and Creating Structs

Structs are similar to tuples. They both can contain elements of different types. In struct, these elements are named. To define the struct:

struct User {
    username: String, //This is a field
    email: String,
    sign_in_count: u64,
    active: bool,
}

We create an instance of a struct by defining each of the fields.

let user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

The order doesn't matter, because the fields are named. (An advantage over tuples.)

We can use dot notation to get specific attributes from a struct (user1.email). If this is mutable we can change it via this way of accessing:

user1.email = String::from("[email protected]")

The entire struct must be mutable. We cannot mark specific fields as mutable.

Using the Field Init Shorthand when Variables and Fields Have the Same Name

Let's say we have a function:

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

we can just simply have it to be:

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Creating Instances From Other Instances With Struct Update Syntax

There will be cases where we want to create a struct with most of an old struct's field with some changed. We can use the struct update syntax.

Instead of:

let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

we can have:

let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    ..user1
};

The .. specifies that the remaining fields not set should have the same values as the fields in the given instance.

Using Tuple Structs without Named Fields to Create Different Types

You can define structs that look like tupes, called tupled structs. They do not have names associated to the fields, rather they types associated to the fields. This is useful when you want to give a tuple a meaning, and make it a different type from other tupes, where field names are redundant.

fn main() {
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Unit-Like Structs Without Any Fields

You can have structs without any fields. This is useful for when you want to implement a trait that doesn't any data itself.

Ownership of Struct Data

The examples above, the fields are owned by the struct. We can have structs with fields that are owned by something else, but to do so we need to use lifetimes. Lifetimes esnure that the data references by a struct is valid for as long as the struct is. This will be further explained later.

An Example Program

Example Programs

Method Syntax

Defining Methods

Let's modify the example in the previous section:

[derive(Debug)]
struct Rect {
    width: u32,
    height: u32,
}

impl Rect {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rect {
        width: 30,
        height: 50,
    };

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

To define the function within a context of Rect, we have to define an impl (implementation) block. We move the area function to this block and change the param to be self.

We use self instead of rect: &Rect because Rust knows the type for &self. (We still need to pass in a reference because methods can take ownership, reference immutably, or reference mutably.)

Having a method that takes ownership is rare. Usually we see this only when the method transforms self into something else and you want to prevent the caller from using the original.

Methods with More Parameters

If you want to use more parameters:

impl Rect {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Associated Functions

We can define functions that do not take in self in the impl block. These are called associated functions because they are associated with the struct. They are functions, not methods because they are not associated with an instance. String::from is an example.

Associated functions are often used for constructors:

impl Rect {
    fn square(size: u32) -> Rect {
        Rect {
            width: size,
            height: size,
        }
    }
}

To call an associated function, we use :: - let sq = Rect::square(3);

Multiple Impl Blocks

We have multiple impl blocks, and all will be considered. Not sure why we'd want to do this, but it is possible.