Understanding Memory Management in Rust: Reference Borrowing
Written on
Chapter 1: Introduction to Memory Management in Rust
In the previous installment of this series, titled "Memory Management in Rust — Part 1: Ownership and Moves," we began to unpack how memory is managed in Rust. We introduced key concepts such as Ownership and Moves, encapsulated in the idea that:
Every value has a single owner that dictates its lifespan.
In this second part, we will delve into the notions of references and reference borrowing. We're nearing the conclusion, so please stick with me! 🦾
What Exactly is a Reference?
A reference acts like a pointer; however, it does not possess the value it points to. Instead, it serves as a memory address where the designated value is stored.
You create references using the ampersand &, and dereference (access the value) using the asterisk *. For example, if we have let variable = 1;, creating a reference would look like let reference = &variable;. Here, reference points to the value of 1, and using *reference will yield 1, the value contained in variable.
A crucial aspect of references is that they cannot outlive the value they point to, which leads us to the concept of borrowing. Essentially, a reference borrows the value from its owner and is expected to return it later.
As you might have inferred, the borrowing concept introduces a degree of flexibility to the principles of ownership and moves (you can find a refresher on those concepts here).
Let's look at an example where we attempt to print all values from a variable named missy_favorite_humans, followed by an attempt to print its size. This will fail because ownership of the values is transferred to the for loop, rendering missy_favorite_humans undefined when we try to invoke the len() method.
Now, let's modify the example to incorporate a reference:
for human in &missy_favorite_humans {
println!("{}", human);
}
In this case, the for loop operates on a reference to missy_favorite_humans. It can access the borrowed values via &missy_favorite_humans and print them. By the end of the loop, &missy_favorite_humans goes out of scope, but missy_favorite_humans retains ownership of its items. Thus, we can successfully execute the statement on line 10.
How Does Rust Ensure References Don't Outlive Their Values?
This is where lifetimes come into play. According to "Rust by Examples," a lifetime is a mechanism used by the compiler (the borrow checker) to validate that all borrows are legitimate. In simpler terms, a lifetime defines the portion of the code where a variable or reference can be utilized safely.
To ensure that a reference does not outlive the value it points to, the compiler verifies that the lifetime of a reference must be encompassed within the lifetime of the variable it refers to. (Take a moment to let that sink in! 🧐)
For example, the following code would be rejected by the compiler:
let r;
{
let x = 42;
r = &x; // Error: r does not live long enough
}
Here, r attempts to reference x, but x goes out of scope before r does.
Now, consider a slightly adjusted example:
let r;
{
let x = 42;
r = &x; // This is valid
}
In this instance, the lifetime rules are adhered to, allowing reference borrowing to succeed.
When using references in function signatures, structs, enums, or implementation blocks, we need to specify their lifetimes. In some scenarios, the Rust compiler can automatically infer lifetimes, allowing us to omit them (this is known as Lifetime Elision).
Lifetimes are denoted with an apostrophe ('). Typically, lowercase letters are used for naming. For instance:
- 'static: This reserved lifetime annotation indicates that references are valid for the entire duration of the program. They are stored in the data segment of the binary, ensuring the referred data never goes out of scope.
It's worth noting that Rust does not support null references.
Types of References
References in Rust come in two varieties: shared references and mutable references. Let’s explore both.
Mutable Reference
Conversely, a mutable reference allows for both reading and modifying the value it points to. You create a mutable reference using the &mut keyword. Mutable references are exclusive; therefore, when a mutable reference to a value exists, no other references of any kind can exist simultaneously for that value, except for mutable references on the existing mutable reference.
The difference between shared and mutable references can be boiled down to this rule: a value can have multiple readers (shared references) or a single writer (mutable reference).
In Conclusion
Throughout this exploration of memory management, we observed that to uphold the safety principles inherent in Rust, stringent rules regarding ownership are enforced. However, some flexibility is introduced through the concepts of moves, references, and borrowing, alongside the rules that facilitate our interaction with memory.
The elegance of Rust lies in these stringent rules, which are enforced at compile time, compelling us to design well-structured and safe programs from the outset.
In Rust, all checks are conducted upfront!
Missy is waiting for me to finalize the mutable reference I hold on the door.
Resources
- Ownership chapter from "The Rust Programming Language"
- "Programming Rust," 2nd edition
- "Rust by Examples"