Skip to main content

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​

  1. At any point you can have either any number of &T or exactly one &mut T
  2. 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 T can 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.

→Ownership ModelWhy Rust has ownership and what the three rules are →Arc<Mutex<T>>Shared mutable state across threads using Arc and Mutex