Rust - References & Borrowing
Overview
Estimated time: 45–60 minutes
Master Rust's borrowing system that allows you to use values without taking ownership. Learn about references, borrowing rules, mutable vs immutable references, and how the borrow checker ensures memory safety at compile time.
Learning Objectives
- Understand references and how they differ from ownership.
- Master the borrowing rules and why they exist.
- Learn the difference between immutable and mutable references.
- Understand the borrow checker and common borrowing patterns.
- Solve ownership problems using references instead of moving values.
Prerequisites
What are References?
References allow you to refer to a value without taking ownership of it. Think of a reference as a pointer that's guaranteed to point to a valid value for the life of that reference.
Creating References
Use the &
operator to create a reference:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference
println!("The length of '{}' is {}.", s1, len); // s1 is still valid!
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope, but it doesn't drop what it refers to
Expected Output:
The length of 'hello' is 5.
References vs Ownership
Compare this to the ownership version we saw earlier:
fn main() {
let s1 = String::from("hello");
// Without references (takes ownership)
let len = calculate_length_ownership(s1); // s1 is moved
// println!("{}", s1); // Error: s1 no longer valid
let s2 = String::from("world");
// With references (borrows)
let len2 = calculate_length_reference(&s2); // s2 is borrowed
println!("s2 is still valid: {}", s2); // s2 is still valid!
println!("Lengths: {}, {}", len, len2);
}
fn calculate_length_ownership(s: String) -> usize {
s.len()
} // s is dropped here
fn calculate_length_reference(s: &String) -> usize {
s.len()
} // s goes out of scope, but nothing is dropped
Expected Output:
s2 is still valid: world
Lengths: 5, 5
The Borrowing Rules
Rust enforces these borrowing rules at compile time:
Rule 1: References Must Always Be Valid
References must always point to valid data:
fn main() {
let reference_to_nothing = dangle(); // This won't compile!
}
fn dangle() -> &String { // Error: missing lifetime specifier
let s = String::from("hello");
&s // We return a reference to data that's about to be dropped
} // s is dropped here, so the reference would be invalid
Correct version:
fn main() {
let string = no_dangle();
println!("{}", string);
}
fn no_dangle() -> String { // Return the String directly, not a reference
let s = String::from("hello");
s // Move s out of the function
}
Expected Output:
hello
Rule 2: Either Multiple Immutable References OR One Mutable Reference
At any given time, you can have either:
- Any number of immutable references, OR
- Exactly one mutable reference
fn main() {
let mut s = String::from("hello");
// Multiple immutable references are OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// This would cause an error if uncommented:
// let r3 = &mut s; // Error: cannot borrow as mutable while immutable refs exist
// After the last use of immutable references, we can create a mutable one
let r4 = &mut s;
r4.push_str(", world");
println!("{}", r4);
}
Expected Output:
hello and hello
hello, world
Immutable References
Multiple Immutable References
You can have as many immutable references as you want:
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("r1: {}, r2: {}, r3: {}", r1, r2, r3);
println!("Original: {}", s); // Original is still accessible
}
Expected Output:
r1: hello, r2: hello, r3: hello
Original: hello
Passing Immutable References to Functions
fn main() {
let name = String::from("Alice");
let age = 30;
print_person_info(&name, &age);
// Both variables are still available
println!("After function call: {} is {}", name, age);
}
fn print_person_info(name: &String, age: &i32) {
println!("Person: {} (age {})", name, age);
}
Expected Output:
Person: Alice (age 30)
After function call: Alice is 30
Mutable References
Creating Mutable References
Use &mut
to create a mutable reference:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Expected Output:
hello, world
Only One Mutable Reference at a Time
You can only have one mutable reference to a value in a particular scope:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // Error: cannot borrow `s` as mutable more than once
r1.push_str(", world");
println!("{}", r1);
// After r1 is no longer used, we can create another mutable reference
let r2 = &mut s;
r2.push_str("!");
println!("{}", r2);
}
Expected Output:
hello, world
hello, world!
Cannot Mix Mutable and Immutable References
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference
println!("{} and {}", r1, r2); // Last use of immutable references
// Now we can create a mutable reference
let r3 = &mut s; // This is OK because r1 and r2 are no longer used
r3.push_str(", world");
println!("{}", r3);
}
Expected Output:
hello and hello
hello, world
Reference Scope and Non-Lexical Lifetimes
References End When Last Used
In modern Rust, references are valid until their last use, not the end of the scope:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1 and r2 end here (last use)
let r3 = &mut s; // This is OK because r1 and r2 are no longer active
r3.push_str(", world");
println!("{}", r3);
// r3 ends here (last use)
// We can create new references now
let r4 = &s;
println!("Final: {}", r4);
}
Expected Output:
hello and hello
hello, world
Final: hello, world
Common Borrowing Patterns
Borrowing in Loops
fn main() {
let words = vec![
String::from("hello"),
String::from("world"),
String::from("rust"),
];
// Borrow each string instead of moving it
for word in &words { // Iterate over references
println!("Word: {}", word);
}
// words is still valid because we borrowed, not moved
println!("Total words: {}", words.len());
// We can iterate again
for (i, word) in words.iter().enumerate() {
println!("{}: {}", i, word);
}
}
Expected Output:
Word: hello
Word: world
Word: rust
Total words: 3
0: hello
1: world
2: rust
Borrowing for Partial Updates
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
// Borrow mutably to modify elements
for num in &mut numbers {
*num *= 2; // Dereference to modify the value
}
println!("Doubled: {:?}", numbers);
// Selective borrowing
let first = &numbers[0];
let last = &numbers[numbers.len() - 1];
println!("First: {}, Last: {}", first, last);
}
Expected Output:
Doubled: [2, 4, 6, 8, 10]
First: 2, Last: 10
Dereferencing
Manual Dereferencing
Use *
to dereference and access the value:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // Dereference y to get the value
println!("x: {}, *y: {}", x, *y);
// With mutable references
let mut a = 10;
let b = &mut a;
*b += 5; // Dereference and modify
println!("a: {}", a);
}
Expected Output:
x: 5, *y: 5
a: 15
Automatic Dereferencing
Rust automatically dereferences in many contexts:
fn main() {
let s = String::from("hello world");
let s_ref = &s;
// These are equivalent
println!("Length 1: {}", s.len());
println!("Length 2: {}", s_ref.len()); // Auto-dereference
println!("Length 3: {}", (*s_ref).len()); // Manual dereference
// Method calls auto-dereference
let starts_with_hello = s_ref.starts_with("hello");
println!("Starts with hello: {}", starts_with_hello);
}
Expected Output:
Length 1: 11
Length 2: 11
Length 3: 11
Starts with hello: true
Borrowing with Structs
Borrowing Struct Fields
struct Person {
name: String,
age: u32,
}
impl Person {
fn get_name(&self) -> &String { // Return a reference to the name
&self.name
}
fn get_age(&self) -> u32 { // Copy the age (u32 is Copy)
self.age
}
fn celebrate_birthday(&mut self) { // Mutable reference to self
self.age += 1;
}
}
fn main() {
let mut person = Person {
name: String::from("Alice"),
age: 30,
};
// Immutable borrows
let name_ref = person.get_name();
let age_copy = person.get_age();
println!("Name: {}, Age: {}", name_ref, age_copy);
// Mutable borrow
person.celebrate_birthday();
println!("After birthday: {} is now {}", person.name, person.age);
}
Expected Output:
Name: Alice, Age: 30
After birthday: Alice is now 31
Partial Borrowing
You can borrow different fields of a struct simultaneously:
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 1, y: 2 };
// Borrow different fields separately
let x_ref = &mut point.x;
let y_ref = &point.y; // Immutable borrow of different field is OK
*x_ref += *y_ref;
println!("Point: ({}, {})", point.x, point.y);
}
Expected Output:
Point: (3, 2)
Advanced Borrowing Scenarios
Borrowing in Function Return Types
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let string2 = "xyz";
let result = longest(&string1, string2);
println!("The longest string is: {}", result);
}
Expected Output:
The longest string is: long string is long
Interior Mutability Pattern
Sometimes you need to mutate data through immutable references:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// Even though `data` is not mutable, we can modify its contents
*data.borrow_mut() += 1;
println!("Value: {}", data.borrow());
// Multiple immutable borrows are allowed
let borrow1 = data.borrow();
let borrow2 = data.borrow();
println!("Borrow 1: {}, Borrow 2: {}", *borrow1, *borrow2);
}
Expected Output:
Value: 6
Borrow 1: 6, Borrow 2: 6
Common Borrowing Mistakes
Borrowing After Move
fn main() {
let s = String::from("hello");
let s_moved = s; // s is moved to s_moved
// let s_ref = &s; // Error: s no longer valid
let s_ref = &s_moved; // This works
println!("Reference: {}", s_ref);
}
Dangling References
fn main() {
let reference;
{
let value = String::from("hello");
// reference = &value; // Error: value doesn't live long enough
}
// println!("{}", reference); // reference would be dangling
// Correct approach: move the value out
let value = {
String::from("hello")
};
let reference = &value;
println!("{}", reference);
}
Expected Output:
hello
Attempting Multiple Mutable Borrows
fn main() {
let mut vec = vec![1, 2, 3];
// This would be an error:
// let first = &mut vec[0];
// let second = &mut vec[1]; // Error: multiple mutable borrows
// Correct approach using split_at_mut
let (left, right) = vec.split_at_mut(1);
let first = &mut left[0];
let second = &mut right[0];
*first += 10;
*second += 20;
println!("Vector: {:?}", vec);
}
Expected Output:
Vector: [11, 22, 3]
Best Practices
Prefer Borrowing Over Ownership
- Use references when you only need to read or temporarily modify data
- Take ownership only when you need to store or consume the value
- Design APIs to accept references when possible
// Good: accepts references
fn process_data(data: &[i32]) -> i32 {
data.iter().sum()
}
// Less flexible: takes ownership
fn process_data_owned(data: Vec) -> i32 {
data.iter().sum()
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Can call multiple times with references
let sum1 = process_data(&numbers);
let sum2 = process_data(&numbers);
// Can still use numbers
println!("Numbers: {:?}, Sum: {}, Sum again: {}", numbers, sum1, sum2);
}
Expected Output:
Numbers: [1, 2, 3, 4, 5], Sum: 15, Sum again: 15
Use Appropriate Reference Types
&T
for reading data&mut T
for modifying data&str
instead of&String
for string parameters&[T]
instead of&Vec
for slice parameters
Performance Implications
Zero-Cost Abstractions
References are zero-cost at runtime - they compile to simple pointer operations:
fn process_large_data(data: &[u8; 1000000]) -> u8 {
// No copying occurs - just passes a pointer
data[0]
}
fn main() {
let large_array = [42u8; 1000000];
// This is as fast as passing a pointer in C
let first_byte = process_large_data(&large_array);
println!("First byte: {}", first_byte);
}
Expected Output:
First byte: 42
Common Pitfalls
- Fighting the borrow checker: Work with Rust's rules, not against them
- Overusing clone(): Often borrowing is the right solution
- Creating unnecessary mutable references: Use immutable when possible
- Misunderstanding reference lifetimes: References must outlive their use
Checks for Understanding
- What's the difference between
&T
and&mut T
? - Can you have multiple mutable references to the same data simultaneously?
- What happens when you try to use a reference after the value it points to is dropped?
- When does Rust automatically dereference references?
- What's the benefit of using references instead of moving values?
Answers
&T
is an immutable reference (read-only),&mut T
is a mutable reference (read-write)- No, you can only have one mutable reference at a time in a given scope
- Compile error - Rust prevents dangling references at compile time
- In method calls and when the context is clear (like comparisons)
- References allow multiple uses of data without transferring ownership, and they're zero-cost at runtime