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:
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.
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.
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:
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.
Let's run the code and see the output:
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.
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
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.