Day 15: Borrowing in Rust
In this article, we explore Rust's borrowing rules, focusing on the interplay between mutable and immutable references. Learn why Rust's strict borrowing rules prevent simultaneous mutable and immutable references, ensuring memory safety and avoiding data races.
Welcome back, fellow Rustaceans 🦀
In our last article, we delved deeper into Rust's ownership model and its impact on how data is moved across a program. If you're coming from a C/C++ background, getting used to Rust's ownership model can take some time. We also explored how to retain variable ownership and avoid the unnecessary transfer of ownership through borrowing. Today, we'll dive further into the concept of borrowing and the rules that govern it.
Let's revisit an example from our previous article.
fn main() {
let s = String::from("Hello, Rust!");
borrow_ownership(&s);
println!("{}", s); // This works now
}
fn borrow_ownership(s: &String) {
println!("{}", s);
}
In this code, the borrow_ownership
function takes a reference to the String
variable s
(&String
). This allows borrow_ownership
to use s
without taking ownership, leaving the original s
in main
valid, so we can print it after the function call.
As we discussed, references in Rust are somewhat similar to pointers in C. You might wonder if using references reintroduces the same issues we had with pointers in C/C++, such as dangling pointers, wild pointers, and null pointer access. So, what's the point of learning Rust?
Although references in Rust are similar to pointers, Rust has an entire system built around references to prevent the pitfalls associated with pointers in C/C++
. The borrow checker
in the Rust compiler enforces strict rules to ensure memory safety. Any violation of these rules results in a compilation error. Let's examine these rules.
The Rules of Borrowing
Rust enforces strict borrowing rules to ensure memory safety:
- At any given time, you can have either one mutable reference or any number of immutable references, but not both.
- References must always be valid.
These rules prevent data races at compile time, ensuring safe and concurrent access to data. Let's look at each of these rules with examples.
So, what is a mutable reference? Similar to variables in Rust, references in Rust are immutable by default. This means that they can only be used to read data, not to modify the original data. Mutable references, denoted by &mut
, allow functions or scopes to modify the borrowed data. However, we can only take mutable references to data that is mutable. Let's look at an example:
fn main() {
let s = String::from("Hello, Rust!");
borrow_mutable_ownership(&mut s);
println!("{}", s); // This will print the modified string
}
fn borrow_mutable_ownership(s: &mut String) {
s.push_str(" Welcome to borrowing!");
}
In this example, the borrow_mutable_ownership
function takes a mutable reference to s
(&mut String
), allowing it to modify the String
. In the borrow_mutable_ownership
function, we attempt to append "Welcome to borrowing!" to the String
and then print it in main
. Let's run this example to see it in action.
The Rust compiler is unhappy again. Examining the error message, it highlights exactly what we discussed: we cannot take mutable references to an immutable variable. This is one of the key differences between references and pointers. The Rust compiler even provides a solution by suggesting we make the String
mutable. Let's give that a try.
fn main() {
let mut s = String::from("Hello, Rust!");
borrow_mutable_ownership(&mut s);
println!("{}", s); // This will print the modified string
}
fn borrow_mutable_ownership(s: &mut String) {
s.push_str(" Welcome to borrowing!");
}
It Works! Great! Now that we understand mutable and immutable references, let's examine the first rule: At any given time, you can have either one mutable reference or any number of immutable references, but not both. Here's an example:
fn main() {
let mut s = String::from("Hello, Rust!");
let r1 = &s; // Immutable reference
let r2 = &s; // Immutable reference
let r3 = &mut s; // This would cause a compile-time error
println!("{}, {}", r1, r2); // This would cause a compile-time error
println!("{}", r3);
}
Here, we declare a mutable String
variable s
. The mut
keyword allows s
to be modified. We create two immutable references r1
and r2
to s
using &s
. These references allow us to read the value of s
but not modify it. Having multiple immutable references is allowed in Rust because they do not change the data they reference. We then try to take a mutable reference r3
and try to print r2, r3, r4
. Let's try to run this code.
I think now you’re starting to understand the frustration I mentioned when getting started with Rust. The Rust compiler complains with an error message: "cannot borrow s
as mutable because it is also borrowed as immutable." So, how can we fix this? One approach is to declare r3
after printing r1
and r2
. This ensures that the immutable references are not used after the mutable reference is created.
fn main() {
let mut s = String::from("Hello, Rust!");
let r1 = &s; // Immutable reference
let r2 = &s; // Immutable reference
println!("{}, {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);
}
Let' try to run this code.
The Ferrous is happy again!!!!
Conclusion
Borrowing is a fundamental aspect of Rust’s ownership model, enabling flexible and efficient code. By understanding and following Rust’s borrowing rules, we can ensure safe and concurrent access to data without the overhead of ownership transfers.
Today, we explored mutable references, and the first rule of borrowing "At any given time, you can have either one mutable reference or any number of immutable references, but not both."
In the next article, we'll dive into the second rule of borrowing: References must always be valid. We'll explore how Rust guarantees that references are never null, thereby preventing issues such as dangling pointers and null pointer exceptions. Stay Tuned
For more articles on Rust, check out InPyjama.
Discussion