References & Borrowing
In the previous lesson, we learned that transferring ownership can be tedious. Imagine having to pass ownership back and forth with every function call! Fortunately, Rust provides references - a way to refer to a value without taking ownership of it. This is called borrowing.
What is a Reference?
A reference is like a pointer: it's an address we can follow to access data stored at that address. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 creates a reference to s1
println!("The length of '{}' is {}", s1, len);
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the data
The &s1 syntax creates a reference that refers to the value
of
s1 but does
not own it. Because it doesn't own it, the value it points to will not be
dropped
when the reference stops
being used.
Immutable References (&T)
By default, references are immutable. You can have multiple immutable references to the same data:
Click Run to execute your code
Mutable References (&mut T)
If you want to modify a borrowed value, you need a mutable reference:
let mut s = String::from("hello");
change(&mut s); // Pass a mutable reference
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Click Run to execute your code
The Borrowing Rules
Rust enforces these rules at compile time to prevent data races:
- You can have any number of immutable references to a value
- You can have only ONE mutable reference to a value
- You cannot have mutable and immutable references at the same time
Why These Rules?
These rules prevent data races, which occur when:
- Two or more pointers access the same data at the same time
- At least one pointer is being used to write to the data
- There's no mechanism to synchronize access to the data
Click Run to execute your code
Reference Scope and Lifetimes
A reference's scope starts where it's introduced and continues through its last use:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // OK! No immutable references are active
println!("{}", r3);
r1
and r2 are not used after the println!, so it's safe
to
create a mutable reference.
Dangling References
In languages with pointers, it's easy to create a dangling pointer - a pointer that references a location in memory that may have been given to someone else. Rust guarantees that references will never be dangling:
// This won't compile!
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // Error! s will be dropped, creating a dangling reference
// }
// Correct version - return the String itself
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out
}
References vs Ownership
| Aspect | Ownership | Immutable Reference | Mutable Reference |
|---|---|---|---|
| Syntax | T |
&T |
&mut T |
| Owns data | Yes | No | No |
| Can modify | Yes (if mut) | No | Yes |
| Multiple allowed | No | Yes | No |
| Drops data | Yes | No | No |
Common Borrowing Mistakes
1. Multiple mutable references
Wrong:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // Error! Cannot have two mutable references
println!("{}, {}", r1, r2);
Fix - Use scopes:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope
let r2 = &mut s; // OK!
2. Mixing mutable and immutable references
Wrong:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // Error! Cannot borrow as mutable
println!("{}, {}", r1, r2);
Fix - Separate usage:
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1 is no longer used after this
let r2 = &mut s; // OK!
println!("{}", r2);
3. Modifying through immutable reference
Wrong:
fn change(s: &String) {
s.push_str("!"); // Error! Cannot mutate through & reference
}
Fix - Use mutable reference:
fn change(s: &mut String) {
s.push_str("!"); // OK!
}
Borrowing Best Practices
- Prefer borrowing over ownership: Use references when you don't need to own the data
- Use immutable references by default: Only use
&mutwhen you need to modify - Keep mutable reference scopes small: Limit the scope of mutable references
- Let references expire: Use references and let them go out of scope before creating new ones
- Trust the compiler: Borrowing errors are caught at compile time with helpful messages
- Think about data flow: Design your functions to minimize mutable state
Exercise: Borrowing Practice
Task: Fix the borrowing errors in the code.
Requirements:
- Fix immutable reference errors
- Fix multiple mutable reference errors
- Fix mixed reference errors
- Use proper reference types
Click Run to execute your code
Show Solution
fn main() {
// Fix 1: Use mutable reference
let mut s = String::from("hello");
change_string(&mut s);
println!("{}", s);
// Fix 2: Separate mutable references
let mut s1 = String::from("world");
let r1 = &mut s1;
r1.push_str("!");
println!("{}", r1);
// r1 is done, now create r2
let r2 = &mut s1;
r2.push_str("!");
println!("{}", r2);
// Fix 3: Separate immutable and mutable
let mut s2 = String::from("Rust");
let r1 = &s2;
println!("{}", r1);
// r1 is done
let r2 = &mut s2;
r2.push_str("!");
println!("{}", r2);
// Fix 4: Use reference
let s3 = String::from("programming");
let len = calculate_length_fixed(&s3);
println!("Length of '{}' is {}", s3, len);
// Fix 5: Use mutable reference
let mut s4 = String::from("hello");
append_world(&mut s4);
println!("{}", s4);
}
fn change_string(s: &mut String) {
s.push_str(", world");
}
fn calculate_length_fixed(s: &String) -> usize {
s.len()
}
fn append_world(s: &mut String) {
s.push_str(" world");
}
Summary
- References let you refer to values without taking ownership
- Borrowing is creating a reference
- Immutable references (&T): Read-only access, multiple allowed
- Mutable references (&mut T): Read-write access, only one at a time
- Three borrowing rules: Multiple immutable OR one mutable, never both
- References must always be valid - no dangling references
- Reference scope ends at last use, not end of block
- Borrowing rules prevent data races at compile time
- The compiler provides helpful error messages for borrowing issues
- References are zero-cost abstractions - no runtime overhead!
What's Next?
You now understand ownership and borrowing - the core of Rust's memory safety! In the next lesson, you'll learn about slices - a special kind of reference that lets you reference a contiguous sequence of elements in a collection rather than the whole collection.
Enjoying these tutorials?