Introduction to References and Borrowing
After covering ownership, it’s essential to dive into references and borrowing, another cornerstone of Rust’s memory safety. If you’re new to Rust, I strongly recommend you first familiarize yourself with ownership before proceeding. For those already acquainted, let’s explore how Rust allows you to use references to manage memory without transferring ownership.
Using References Instead of Moving Values
In Rust, when you pass a variable to a function, you typically transfer ownership of that variable to the function. However, sometimes you want to let the function use a variable without giving up ownership. This is where references and borrowing come into play.
Consider the following function:
fn do_something(s: &String) {
println!("{}", s);
}
RustHere, the &
symbol is used to create a reference to the String
rather than moving the String
itself. The reference points to the value without taking ownership of it. This means the original variable retains ownership, and the function merely borrows it for a while.
let s1 = String::from("abc");
do_something(&s1);
RustIn this example, s1
remains valid after being passed to do_something
because we’re passing a reference to s1
, not the value itself. The function do_something
borrows the reference, and once the function finishes, the borrowing ends, and the reference goes out of scope.
Here’s how this looks:
s (reference) s1 (owner)
|-----|---| |-----|---|
| ptr | | ------> | ptr | |
|-----|---| | len | 3 |
| cap | 3 |
|-----|---|
PlaintextWhat’s Happening Under the Hood?
When you create a reference to s1
, Rust internally creates a pointer to the value s1
. This pointer allows the function to access the value without owning it. Rust automatically manages the creation and destruction of these pointers, ensuring they are always valid.
One of Rust’s most powerful features is its ability to enforce lifetimes, ensuring that references do not outlive the data they point to. This avoids common bugs in other languages, such as dangling pointers or null references.
Mutable References
By default, references in Rust are immutable. Even if the value being referenced is mutable, the reference itself is immutable. If you need to modify the value through the reference, you must use a mutable reference, denoted by &mut
.
fn do_something(s: &mut String) {
s.push_str("def");
}
let mut s1 = String::from("abc");
do_something(&mut s1);
println!("{}", s1);
RustIn this code, we pass a mutable reference to s1
to the do_something
function. The function can now modify s1
because it has been given a mutable reference. Here’s what this looks like:
s (mutable reference) s1 (owner)
|-----|---| |-----|---|
| ptr | | ------> | ptr | |
|-----|---| | len | 3 |
| cap | 3 |
|-----|---|
PlaintextThe Dot Operator and Dereferencing
Rust has a unique feature with the dot operator (.
). When you use the dot operator for method calls or accessing fields, Rust automatically dereferences down to the actual value, so you don’t need to worry whether you’re dealing with a value or a reference.
However, if you need to manually dereference a reference, you can use the *
operator:
fn do_something(s: &mut String) {
(*s).push_str("def");
}
RustThe dereference operator (*
) has lower precedence than the dot operator. This means you sometimes need to use parentheses to clarify your intentions. For example:
fn do_something(s: &mut String) {
*s = String::from("def");
}
RustHere, we manually dereference s
and replace it with a new value.
Rust’s Rules on References
Rust enforces a special rule to keep your code safe: you can have either one mutable reference or multiple immutable references to a variable, but not both at the same time. This rule prevents data races, especially in multithreaded programming.
References in Multithreaded Contexts
Let’s consider a scenario where two threads are borrowing references to the same variable:
Thread A
ref1
|----|--|
| ptr| |
|----|--| ----------> Thread C
s1
Thread B --------> |----|----| |-----|
ref2 | ptr | | --> | a |
|----|--| | len | 3 | | b |
| ptr| | | cap | 3 | | c |
|----|--| |----|----| |-----|
PlaintextIn a multithreaded environment, having multiple mutable references to the same variable can lead to unpredictable behavior, data corruption, or crashes. Rust’s borrowing rules prevent this by ensuring that only one mutable reference exists at any time, or multiple immutable references exist simultaneously but without any mutable reference.
Conclusion
References and borrowing in Rust are essential concepts for safe and efficient memory management. By understanding how references work, you can allow functions to access data without transferring ownership, maintain memory safety, and prevent common bugs such as null pointers and data races. Rust’s strict rules about borrowing and mutable references ensure that your code is both safe and performant, making it an excellent choice for writing concurrent and systems-level programs.