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
You met &mut back in Ownership & Borrowing: to modify borrowed data, the reference itself must be mutable. Here we pick that up and look at what the compiler enforces around it.
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");
}
Note that both the binding (let mut s) and the reference (&mut s) must be marked mutable — a &mut to an immutable binding won't compile. That single restriction is what powers the rules below.
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!
Next Steps
With borrowing mastered, you're ready for closures — anonymous functions that capture variables from their environment. Because closures borrow (or move) the values they capture, the borrowing rules you just learned are exactly what govern how they behave.
Next lesson
Closures
Rust closures tutorial — learn how closures capture variables, understand Fn, FnMut, and FnOnce traits, and use closures with iterators
25 min