Borrowing in Depth
Building on what we learned about ownership, this lesson takes a deeper look at borrowing - one of Rust's most powerful features for writing safe, efficient code without copying data.
Why Borrowing Matters
Without borrowing, you'd need to pass ownership around constantly, which can be inconvenient:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
// s1 is moved, can't use it anymore!
println!("The length of '{}' is {}.", s2, len);
}
// Awkward: returns the string back along with the length
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
With borrowing, this becomes much cleaner:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
// s1 is still valid!
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Immutable References
By default, references are immutable - you can read but not modify:
fn main() {
let s = String::from("hello");
// Create immutable reference
let r1 = &s;
let r2 = &s; // Multiple immutable refs are OK
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
println!("Original: {}", s); // s is still valid
}
Mutable References
To modify borrowed data, use &mut:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // Prints "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
The Borrowing Rules
Rust enforces these rules at compile time:
Rule 1: One Mutable OR Many Immutable
You can have either:
- One mutable reference, OR
- Any number of immutable references
But never both at the same time:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK - first immutable ref
let r2 = &s; // OK - second immutable ref
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used
let r3 = &mut s; // OK - mutable ref (no immutable refs active)
println!("{}", r3);
}
This prevents data races:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
// let r2 = &mut s; // ERROR! Can't have mutable while immutable exists
println!("{}", r1);
}
Rule 2: References Must Be Valid
References must always point to valid data (no dangling references):
// This won't compile!
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // ERROR: s is dropped, reference would be invalid
// }
// Instead, return the owned value:
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out
}
fn main() {
let s = no_dangle();
println!("{}", s);
}
Non-Lexical Lifetimes (NLL)
Modern Rust uses NLL - references are considered "active" only until their last use, not until the end of scope:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2's last use is here ^^^
// This works because r1 and r2 are no longer "live"
let r3 = &mut s;
println!("{}", r3);
}
Reborrowing
You can reborrow from a mutable reference:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// Reborrow: create immutable ref from mutable ref
let r2 = &*r1; // or just: let r2 = &r1;
println!("{}", r2);
// r1 is still valid after r2 is done
r1.push_str(" world");
println!("{}", r1);
}
Borrowing in Structs
Structs can hold references, but need lifetime annotations:
// Simple case: owned data (no lifetimes needed)
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Alice"),
age: 30,
};
println!("User: {}, Age: {}", user.name, user.age);
}
Borrowing Patterns
Pattern 1: Read-Only Access
fn print_info(data: &Vec<i32>) {
for item in data {
println!("{}", item);
}
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
print_info(&numbers);
print_info(&numbers); // Can borrow again
}
Pattern 2: Modify in Place
fn double_values(data: &mut Vec<i32>) {
for item in data.iter_mut() {
*item *= 2;
}
}
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
double_values(&mut numbers);
println!("{:?}", numbers); // [2, 4, 6, 8, 10]
}
Pattern 3: Split Borrowing
You can borrow different parts of a struct simultaneously:
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 0, y: 0 };
let x_ref = &mut point.x;
let y_ref = &mut point.y; // OK! Different fields
*x_ref = 10;
*y_ref = 20;
println!("Point: ({}, {})", point.x, point.y);
}
Common Borrowing Errors
Error: Borrowed Value Moved
fn main() {
let s = String::from("hello");
let r = &s;
// let s2 = s; // ERROR: can't move while borrowed
println!("{}", r); // r still in use
}
Error: Mutable Borrow While Immutable Exists
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
// v.push(4); // ERROR: can't mutate while immutably borrowed
println!("First: {}", first);
}
Practice Exercise
fn main() {
let mut message = String::from("Hello");
// Multiple immutable borrows
let len = get_length(&message);
let first_char = get_first_char(&message);
println!("Length: {}, First char: {:?}", len, first_char);
// Mutable borrow after immutable borrows are done
append_exclamation(&mut message);
println!("Final: {}", message);
}
fn get_length(s: &String) -> usize {
s.len()
}
fn get_first_char(s: &String) -> Option<char> {
s.chars().next()
}
fn append_exclamation(s: &mut String) {
s.push('!');
}
Key Takeaways
- Borrowing lets you use data without taking ownership
&Tcreates an immutable reference (read-only)&mut Tcreates a mutable reference (read-write)- You can have many
&TOR one&mut T, never both - References must always point to valid data
- NLL makes the borrow checker smarter about when refs are "live"
- Understanding borrowing is essential for writing idiomatic Rust
Master borrowing and you'll write safe, efficient Rust code!