Understanding References and Borrowing in Rust
References and borrowing let Rust code access data without taking ownership of it. These concepts are fundamental to Rust’s memory-safety model and allow programs to share and mutate data under rules that the compiler can verify.
What Is a Reference in Rust?
A reference points to a value owned elsewhere without becoming responsible for dropping that value. References use & syntax and are either immutable or mutable:
- Immutable reference (
&T): allows read-only access. - Mutable reference (
&mut T): allows mutation while enforcing exclusive access rules.
Immutable References
Immutable references let a function inspect a value without consuming it:
fn main() {
let s = String::from("Rust");
let len = calculate_length(&s);
println!("The length of '{}' is {}.", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}Key points:
&sborrowssimmutably.calculate_lengthcan read the string but cannot modify it.- Ownership remains with
s, so it can still be used after the function call.
For APIs that only need string data, &str is often more general than &String:
fn calculate_length(s: &str) -> usize {
s.len()
}Mutable References
A mutable reference lets borrowed data be changed:
fn main() {
let mut s = String::from("Hello");
change(&mut s);
println!("{}", s);
}
fn change(s: &mut String) {
s.push_str(", world!");
}Here &mut s grants temporary mutable access to the function.
Borrowing Rules
Rust enforces rules that prevent conflicting access:
- You can have multiple immutable references when there is no overlapping mutable reference.
- You can have a mutable reference only when no other conflicting references are active.
- A reference may never outlive the value it refers to.
These rules are based on the actual lifetimes of references, not simply the braces around a block. With non-lexical lifetimes, a borrow can end after its final use.
Multiple Immutable References
Read-only access can be shared:
fn main() {
let s = String::from("Rust");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
}Because neither reference can mutate s, there is no write conflict.
Exclusive Mutable Access
Overlapping mutable references are rejected:
fn main() {
let mut s = String::from("Rust");
let r1 = &mut s;
// let r2 = &mut s; // error if r1 is still active
println!("{}", r1);
}This restriction is one of the mechanisms Rust uses to prevent data races.
References Must Stay Valid
Rust does not allow a reference to outlive the value it points to:
fn main() {
let r;
{
let x = 5;
// r = &x; // would be invalid outside this block
}
// println!("{}", r);
}The compiler rejects code that could create a dangling reference.
Slices Borrow Part of a Collection
A slice is a reference to a contiguous portion of data:
fn main() {
let s = String::from("Hello, world!");
let hello = &s[0..5];
let world = &s[7..12];
println!("{} {}", hello, world);
}String slicing uses byte indexes, so take care with UTF-8 text: indexes must fall on valid character boundaries or the operation will panic.
Common Borrowing Errors
-
Conflicting references: code tries to mutate a value while another active borrow requires incompatible access. Fix: shorten the borrow, reorder operations, or restructure data so the accesses do not overlap.
-
Dangling-reference attempts: a reference would live longer than the referenced data. Fix: return owned data or ensure the referenced value has a sufficiently long lifetime.
-
Borrow-checker errors around collections: a collection is borrowed while code also tries to mutate it. Fix: separate lookup and mutation phases, use APIs designed for entry-based mutation, or change the data layout.
Best Practices
- Prefer immutable borrowing when mutation is not required.
- Keep mutable borrows as narrow as practical.
- Accept slices such as
&strand&[T]when a function does not require ownership or a specific container type. - Treat borrow-checker diagnostics as information about conflicting ownership assumptions rather than bypassing them with unnecessary clones.
Conclusion
References and borrowing allow Rust programs to share data efficiently while preserving strong memory-safety guarantees. Once you understand when a borrow begins and ends, and why mutable access must be exclusive, many Rust design patterns become much more intuitive.