Rust Rust Series

Understanding References and Borrowing in Rust

In This Article

The blog delves into Rust's concepts of references and borrowing, explaining how they allow functions to access values without transferring ownership. It covers mutable references, the dot operator, and Rust’s borrowing rules, especially in multithreaded scenarios. With a focus on memory safety and efficiency, the content highlights Rust's robust approach to managing references and ensuring safe concurrency.

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);
}
Rust

Here, 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);
Rust

In 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 |
                    |-----|---|
Plaintext

What’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);
Rust

In 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 |
                    |-----|---|
Plaintext

The 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");
}
Rust

The 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");
}
Rust

Here, 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   |
    |----|--|             |----|----|     |-----|
Plaintext

In 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.