1. What are References and Borrowing? #
References #
A reference is like a pointer - it’s an address that allows you to access data stored elsewhere without taking ownership of it. Unlike pointers in other languages, Rust references are guaranteed to point to valid values for their entire lifetime.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 creates a reference
println!("The length of '{s1}' is {len}."); // s1 is still valid here
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // s goes out of scope, but the String it points to is NOT dropped
Borrowing #
Borrowing is the act of creating a reference. Just like in real life, if someone owns something, you can borrow it, use it, and then give it back without taking ownership.
Key Insight: When functions take references as parameters, you don’t need to return the value to give back ownership because you never had ownership in the first place!
2. Types of References #
Immutable References (&T) #
By default, references are immutable - you cannot modify the data they point to.
fn main() {
let s = String::from("hello");
let r1 = &s; // immutable reference
println!("{}", r1); // OK - reading is allowed
// r1.push_str(", world"); // ERROR! Cannot modify through immutable reference
}
Mutable References (&mut T) #
Mutable references allow you to modify the borrowed data, but with strict rules.
fn main() {
let mut s = String::from("hello"); // s must be mutable
change(&mut s); // pass mutable reference
println!("{}", s); // prints "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world"); // OK - can modify through mutable reference
}
Reference Scope #
A reference’s scope starts when it’s introduced and ends at its last usage, not necessarily at the end of the code block.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 scope ends here (last usage)
let r3 = &mut s; // OK - no overlap with r1, r2
println!("{}", r3);
}
3. Multiple Mutable References: The Strict Rules #
Cannot Have Multiple Mutable References Simultaneously #
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
println!("{}, {}", r1, r2);
}
Why This Restriction Exists:
- Prevents data races at compile time
- A data race occurs when:
- Two+ pointers access the same data simultaneously
- At least one pointer is writing to the data
- No synchronization mechanism exists
Can Have Multiple Mutable References in Different Scopes #
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
// use r1
} // r1 goes out of scope
let r2 = &mut s; // OK - not simultaneous
}
4. Multiple Immutable References: The Flexible Rules #
Can Have Multiple Immutable References #
fn main() {
let s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
let r3 = &s; // OK - multiple immutable references allowed
println!("{}, {}, {}", r1, r2, r3);
}
Why This is Safe:
- Reading data doesn’t affect other readers
- No one can modify the data through immutable references
- No risk of data races
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
let r3 = &mut s; // ERROR! Cannot have mutable reference while immutable ones exist
println!("{}, {}, {}", r1, r2, r3);
}
Why This is Forbidden:
- Users of immutable references expect the data to never change
- A mutable reference could modify the data while immutable references exist
- This would violate the “no surprise changes” guarantee
Sequential Usage is Fine (Non-Overlapping Scopes) #
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // last usage of r1 and r2
// r1 and r2 scope ends here
let r3 = &mut s; // OK - no overlap with r1, r2
println!("{}", r3);
}
5. Key Memory Safety Guarantees #
No Dangling References #
Rust prevents references to deallocated memory:
fn dangle() -> &String { // ERROR! Missing lifetime specifier
let s = String::from("hello");
&s // s will be dropped, reference would be invalid
}
fn no_dangle() -> String { // OK - return owned value
let s = String::from("hello");
s // ownership moved out
}
6. The Golden Rules of References #
-
At any time, you can have EITHER:
- One mutable reference, OR
- Any number of immutable references
-
References must always be valid (no dangling references)
-
Reference scopes end at last usage, not at closing braces
-
Functions with reference parameters don’t need to return values to give back ownership
7. Common Patterns and Best Practices #
Function Parameters #
// Good: Take reference when you only need to read
fn get_length(s: &String) -> usize {
s.len()
}
// Good: Take mutable reference when you need to modify
fn append_world(s: &mut String) {
s.push_str(", world");
}
// Avoid: Taking ownership unless you need to consume the value
fn bad_length(s: String) -> usize { // Takes ownership unnecessarily
s.len()
} // s is dropped here - wasteful!
Working with Scopes #
fn main() {
let mut data = String::from("hello");
// Pattern: Use immutable references for reading
{
let r1 = &data;
let r2 = &data;
println!("Reading: {} {}", r1, r2);
} // immutable references end
// Pattern: Use mutable reference for modification
{
let r3 = &mut data;
r3.push_str(", world");
println!("Modified: {}", r3);
} // mutable reference ends
println!("Final: {}", data);
}
Summary #
References and borrowing are Rust’s way of allowing safe access to data without transferring ownership. The strict rules around mutable and immutable references prevent data races and ensure memory safety at compile time. While these rules might seem restrictive initially, they eliminate entire classes of bugs that plague other systems programming languages.
The key insight is that:
- Rust enforces these rules at compile time
- Catches potential issues before your code ever runs
- Leading to safer and more reliable programs