Borrowing, in its mutable and immutable forms, is a cornerstone of Rust’s promise for memory safety without a garbage collector. While it might seem daunting at first, getting a firm grasp on this concept is essential for any budding Rust developer.
In this article, we’ll break down the basics, provide clear examples, and help you navigate the intricacies of borrowing.
Let’s get started!
Ownership in Rust
Before we delve into borrowing, it's crucial to understand ownership. In Rust:
- Each value has a unique owner.
- The value will be dropped (i.e., its memory is freed) when its owner goes out of scope.
- Ownership can be transferred from one variable to another with the
moveoperation.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2, s1 is no longer valid here
println!("{}", s2); // This is fine
// println!("{}", s1); // This would throw a compile-time error
}
Basics of Moving Ownership
When you assign a value from one variable to another or pass a value to a function, the “ownership” of that value is moved. After the move, the original variable can no longer be used.
Here’s a simple example:
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // This will cause a compile-time error!
In the code above, the ownership of the string "hello" is initially with s1. But once we assign s1 to s2, the ownership is moved to s2. This means that s1 is no longer valid, and attempting to use it will cause a compile-time error.
Why Move Instead of Copy?
Rust’s default behavior is to move rather than copy data. This might seem counterintuitive, especially if you come from languages where data is copied implicitly. So why does Rust do this?
- Efficiency: Moving ownership is faster than making a copy of data, especially for complex types like
String. It involves just transferring the pointer, length, and capacity without copying the actual data. - Avoiding Double Free Errors: If Rust copied the data implicitly for types that manage resources (like
String), there could be two variables responsible for freeing the same memory space, leading to double free errors.
Copy Trait
For simple types like integers, copying the data is straightforward and without risk. Rust provides a special trait called Copy for such types. When a type implements the Copy trait, it will be copied instead of moved.
For instance:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // This works because integers are Copy.
It’s important to note that you cannot implement both the Copy and Drop traits for a type. If a type requires custom logic to release resources (like memory) when it goes out of scope, it cannot be Copy.
Implications for Functions
The move operation also comes into play when passing values to functions:
fn take_ownership(s: String) {}
let s1 = String::from("hello");
take_ownership(s1);
println!("{}", s1); // Compile-time error! Ownership was moved to the function.
The function take_ownership takes ownership of the string. Once the function is called, s1 no longer has ownership, and thus it can't be used.
Returning Values and Ownership
Functions can also transfer ownership back to the caller:
fn give_ownership() -> String {
String::from("hello")
}
let s2 = give_ownership();
println!("{}", s2); // This works! s2 now owns the string.
In the code above, the function give_ownership creates a string and transfers its ownership to s2.
Borrowing
Instead of transferring ownership, often, we just want to access the data without owning it. This is where borrowing comes into play.
Immutable Borrowing
You can borrow a value immutably using the & operator. This allows multiple references to read from the same data, but none of them can modify it.
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // We are borrowing s immutably here
println!("The length of '{}' is {}.", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
In this example, the s string is borrowed by the calculate_length function, which then returns the length without taking ownership.
Mutable Borrowing
There are situations where you want to modify the borrowed data. This is where mutable borrowing comes into play. You can borrow a value mutably using the &mut operator.
fn main() {
let mut s = String::from("hello");
append_world(&mut s); // We are borrowing s mutably here
println!("{}", s); // prints "hello, world"
}
fn append_world(s: &mut String) {
s.push_str(", world");
}
The append_world function borrows the string s mutably and appends ", world" to it.
Rules of Borrowing
- Either one mutable borrow or multiple immutable borrows: At any given time, you can either have one mutable reference or any number of immutable references to a particular data, but not both. This ensures data races can’t occur.
let mut s = String::from("hello"); let r1 = &mut s; // let r2 = &mut s;
// This is not allowed!
2. You can’t mix immutable and mutable borrows: Once something has been mutably borrowed, you can’t borrow it immutably until the mutable borrow ends.
let mut s = String::from("hello"); let r1 = &s; // let r2 = &mut s;
// This is not allowed!
3. Dangling references are prevented: The Rust compiler ensures that references never outlive the data they point to.
Why These Rules?
The rules around borrowing in Rust are built to ensure memory safety without sacrificing performance. By enforcing these rules at compile-time:
- Concurrency becomes safer: Since Rust guarantees that there’s either a single mutable reference or multiple immutable ones, you won’t run into issues of data races.
- No garbage collector is needed: Rust’s ownership and borrowing system allow it to manage memory efficiently without needing a garbage collector, resulting in predictable performance.
Beyond the Basics
Once you grasp the fundamental concepts of borrowing in Rust, it’s essential to explore more advanced aspects of the language. These provide additional context and depth to the borrowing mechanism.
Lifetimes
Lifetimes are a way to express the scope of validity of references. They ensure that references don’t outlive the data they point to, preventing dangling references.
For example:
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
Here, the 'a is a lifetime annotation that denotes the scope of the reference. It indicates that the returned reference from the longest function will not outlive either of its input references.



