Structs and ownership

Now we have a good understanding of structs and how to define them and work with them. Let's also understand how structs work in terms of ownership and how they're stored in memory.

Let's have an example:

struct Bird {
    name: String,
    age: u32,
}

In the example above, we have a struct Bird with two fields: name of type String and age of type u32.

When we create an instance of the Bird struct, the name field, which is a String, stores the pointer, length, and capacity on the stack, while the actual string data is stored on the heap. The age field, being a primitive type (u32), is stored on the stack directly.

fn main() {
    let bird = Bird {
        name: String::from("Sparrow"),
        age: 2,
    };
}

When the bird instance goes out of scope, all data nested inside the struct will be dropped, including the name field, which will be de-allocated from the heap.

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Moving ownership

Initially when you declare a struct with named fields, the struct owns all of the data inside of it in the case above, the variable bird owns all of the data, including the name field.

However, you can still move ownership of a part of the struct to a new owner, whether it be a variable, a function or another struct. Here's an example of moving ownership of the name field from one struct to another:

fn main() {
    let bird = Bird {
        name: String::from("Sparrow"),
        age: 2,
    };
 
    let new_bird = Bird {
        name: bird.name,
        age: 3,
    };
    // bird.name is not accessible anymore
 
    println!("The new bird's name is {}", new_bird.name);
}

In this example we have two instances of the Bird struct: bird and new_bird. We move the ownership of the name field from bird to new_bird and we're able to use the new_bird.name field.

The code works fine, but what if we wanted to use the bird.name after moving the ownership to new_bird? We'll get a compile error because we're trying to use a value after moving its ownership.

fn main() {
    let bird = Bird {
        name: String::from("Sparrow"),
        age: 2,
    };
 
    let new_bird = Bird {
        name: bird.name,
        age: 3,
    };
 
    println!("The new bird's name is {}", new_bird.name);
    println!("The old bird's name is {}", bird.name);
}

Let's run the code and see the output:

Ownership error Ownership error

As you can see from the error message value borrowed here after move, Rust doesn't allow you to use the bird.name after moving the ownership to new_bird.

So it's important to keep in mind that nested values can independently be moved to new owners, and you should be careful when moving ownership of nested values.

Using references with structs

Structs can also store references to data instead of taking ownership of it. This can be useful when you want access of some data that is expensive to clone but the struct still needs to have some sort of access to the data.

But there's a catch, you need to specify what's called a lifetime specifier when using references in structs. This is because the Rust compiler needs to know how long the reference will live and make sure it doesn't outlive the data it's referencing.

We'll cover lifetimes in more detail in the next lessons; you don't need to completely understand them now, just know that they are used to tell the Rust compiler how long a reference will live so that it can verify that the reference will not outlive the data it's referencing and cause a dangling reference.

A lifetime specifier can be specified using a single quote ' followed by a lifetime name, for example 'a. Lifetime names are generally very short and are often single letters, 'a is a common lifetime name used in real-life scenarios.

struct Bird<'a> {
    name: &'a str,
    age: u32,
}
 
fn main() {
    let name = "Sparrow";
    let bird = Bird {
        name,
        age: 2,
    };
 
    println!("The bird's name is {}", bird.name);
}

In this example, we define a struct Bird with a reference to a string &str instead of an owned String. We then create a variable name and assign it a string value "Sparrow". We then created an instance of the Bird struct and assign the name field to the name variable.

This code works and compiles successfully because the Rust compiler can 100% guarantee that the reference will live more than the struct instance.

How does the compiler know the name slice will outlive the bird instance?

Every string literal in Rust has a 'static lifetime, meaning it will live for the entire duration of the program.

So in this case, name is a string literal, so the Rust compiler can embed it into the program's output binary and it will live for the entire duration of the program and will be accessible until the program ends.

Since name has a 'static lifetime (entire program), and bird requires a lifetime of 'a which outlives the bird instance, it is safe to use the 'static lifetime in the bird instance, satisfying the compiler's requirements.

Also, accessing name and bird.name are both valid because they are just references to the same data in memory and the data is not moved when the struct is created, instead a cheap reference is created and stored in the struct.

Let's run the code:

Using references with structs Using references with structs

As you can see, the code works fine and we're able to use the reference in the struct instance and the compiler isn't complaining.

Lifetimes are a great feature in Rust; they let you reuse data by references and avoid unnecessary deep clones, while ensuring safety and making sure you don't have any dangling references.

Conclusion

In this chapter, we've learned how to define and use structs, we've also learned how to implement methods on structs and how to run them. We've also touched upon ownership and lifetimes in structs and how they work.

In the next lesson, we're going to learn about enums, a Rust data type that allows you to define a type by enumerating its possible variants.

Enums have a few similarities with structs but have their own use cases and are used in different scenarios.