Borrowing & References
TL;DR Instead of transferring ownership, you can borrow a value via a reference. Rust enforces: either many shared borrows (&T) or exactly one exclusive borrow (&mut T) — never both at once.
Reference Rules
- At any point you can have either any number of
&Tor exactly one&mut T - References must always be valid (no dangling pointers)
let mut s = String::from("hello");
let r1 = &s; // ok — shared borrow
let r2 = &s; // ok — multiple shared borrows are fine
println!("{r1} {r2}");
// r1 and r2 last used above — their borrow ends here (NLL)
let r3 = &mut s; // ok — no active shared borrows at this point
r3.push_str(" world");
Shared Borrows (&T)
- Read-only access
- Multiple simultaneous borrows allowed
- Common in function signatures when you only need to read
fn length(s: &String) -> usize {
s.len() // doesn't need ownership, just reads
}
let s = String::from("hello");
let len = length(&s);
println!("{s} has {len} chars"); // s still valid
Prefer &str over &String in function signatures — &str is a string slice and accepts both String references and string literals.
Exclusive Borrows (&mut T)
- Read-write access
- Only one
&mut Tcan exist at a time for a given value - The original binding must be declared
mut
fn append(s: &mut String) {
s.push_str(" world");
}
let mut s = String::from("hello");
append(&mut s);
println!("{s}"); // "hello world"
Why the restriction? Prevents data races: if two threads could both hold &mut T to the same data, you'd have a race condition. The compiler catches this at compile time.
Non-Lexical Lifetimes (NLL)
Since Rust 2018, the borrow checker uses NLL: borrows end at their last use, not at the end of their scope block.
let mut v = vec![1, 2, 3];
let first = &v[0]; // shared borrow
println!("{first}"); // last use of `first` — borrow ends here
v.push(4); // ok: no active shared borrow
Before NLL this would have been a compile error.
Borrow Splitting
The compiler understands struct fields as separate borrows:
struct Point { x: f64, y: f64 }
let mut p = Point { x: 1.0, y: 2.0 };
let x = &mut p.x;
let y = &mut p.y; // ok — different fields
*x += 1.0;
*y += 1.0;
Same field is not allowed:
let x1 = &mut p.x;
let x2 = &mut p.x; // error: p.x already borrowed mutably
Common Pitfalls
Returning a reference to a local:
// won't compile
fn bad() -> &String {
let s = String::from("hello");
&s // s is dropped at end of function — dangling reference
}
Return the owned String instead, or ensure the data lives long enough.
RefCell<T> — runtime borrow checking:
When you need interior mutability (mutate through a shared reference), RefCell<T> moves the borrow check to runtime. It panics if the rules are violated instead of failing at compile time. Use it sparingly.