What Is Borrowing in Rust?
Borrowing allows you to access a value without taking ownership of it. Instead of transferring ownership, Rust provides references (&
) that let you “borrow” a value temporarily. Borrowing is particularly useful when you want to read or modify a value without creating a copy or moving ownership.
There are two types of borrowing in Rust:
- Immutable Borrowing: Accessing a value without modifying it.
- Mutable Borrowing: Accessing and modifying a value.
Immutable Borrowing
Immutable borrowing allows you to access a value without changing it. You create an immutable reference using the & symbol.
Example 1: Immutable Borrowing
fn main() {
let s = String::from("Rust");
let len = calculate_length(&s); // Borrowing s as an immutable reference
println!("The length of '{}' is {}.", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len() // Accessing the borrowed value
}
Output:
The length of 'Rust' is 4.
Explanation:
- The function calculate_length borrows the string s as an immutable reference.
- The original value
s
remains valid after the borrowing.
Rules of Immutable Borrowing
- You can have multiple immutable references to a value at the same time.
- You cannot modify the borrowed value through an immutable reference.
Example 2: Multiple Immutable References
fn main() {
let s = String::from("Rust");
let r1 = &s; // Immutable reference 1
let r2 = &s; // Immutable reference 2
println!("r1: {}, r2: {}", r1, r2); // Both references are valid
}
Mutable Borrowing
Mutable borrowing allows you to modify the borrowed value. You create a mutable reference using the &mut symbol.
Example 3: Mutable Borrowing
fn main() {
let mut s = String::from("Rust");
modify(&mut s); // Borrowing s as a mutable reference
println!("Modified string: {}", s);
}
fn modify(s: &mut String) {
s.push_str(" Programming!"); // Modifying the borrowed value
}
Output:
Modified string: Rust Programming!
Explanation:
- The function modify borrows the string
s
as a mutable reference and modifies it. - The changes persist after the function call.
Rules of Mutable Borrowing
- You can have only one mutable reference to a value at a time.
- You cannot mix mutable and immutable references to the same value.
Example 4: Mutable Borrowing Restrictions
fn main() {
let mut s = String::from("Rust");
let r1 = &mut s; // Mutable reference
// let r2 = &s; // Error: Cannot borrow as immutable while mutable borrow exists
println!("{}", r1);
// Mutable references must end before creating a new one
let r2 = &mut s; // Valid now, as r1 is no longer used
println!("{}", r2);
}
Borrowing in Functions
Functions can borrow values as arguments, avoiding ownership transfer. This allows the caller to retain ownership while the function operates on borrowed data.
Example 5: Borrowing in Functions
fn main() {
let mut s = String::from("Hello");
print_length(&s); // Immutable borrowing
append_world(&mut s); // Mutable borrowing
println!("Modified string: {}", s);
}
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn append_world(s: &mut String) {
s.push_str(", world!");
}
Output:
Length: 5
Modified string: Hello, world!
Borrowing and Scopes
Borrowing follows strict scoping rules. A reference becomes invalid as soon as it goes out of scope. This ensures memory safety.
Example 6: Borrowing and Scoping
fn main() {
let mut s = String::from("Rust");
{
let r = &mut s; // Mutable reference exists in this scope
r.push_str(" is awesome!");
} // r goes out of scope here, allowing new references
let r2 = &s; // Immutable reference is now allowed
println!("{}", r2);
}
Dangling References
Rust prevents dangling references by ensuring that references are always valid. A dangling reference occurs when a reference points to a memory location that has been freed. Rust’s compiler catches such errors at compile time.
Example 7: Dangling Reference Prevention
fn main() {
let r = dangling_reference();
println!("{}", r); // Compiler error
}
fn dangling_reference() -> &String {
let s = String::from("Rust");
&s // Error: s goes out of scope here, making the reference invalid
}
Borrowing in Loops
Borrowing is often used in loops to iterate over collections without consuming them.
Example 8: Borrowing in Loops
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum(); // Borrowing values in the vector
println!("Sum: {}", sum);
}