05 / 20 · Day 2
Day 2 · Concept 05

References & borrowing

How to use a value without owning it. &T is a shared, read-only reference — there can be many. &mut T is an exclusive, read-write reference — there can be exactly one. Never both at once. This is the aliasing-XOR-mutation rule and the foundation of Rust's safety.


1 · The intuition

Ownership says only one variable owns a value at a time. Borrowing lets other code use the value without taking ownership. The compiler enforces two rules: (a) at any one moment, you can have many &T (shared) or one &mut T (exclusive), never both; (b) references must not outlive the value they point at.

Why this is enough. Data races require simultaneous read and write to the same memory. Rule (a) makes that impossible at compile time. Use-after-free requires a reference to outlive its referent. Rule (b) prevents that.

2 · Shared references — read many

rust src/main.rs
fn print_len(s: &String) {
    println!("len = {}", s.len());
    // s is a shared reference — we can read but not mutate
}

fn main() {
    let owned = String::from("hello");
    print_len(&owned);
    print_len(&owned);   // call again — still owned by main
    println!("{}", owned); // still ours
}

3 · Mutable references — exactly one

rust src/main.rs
fn append_bang(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut owned = String::from("hello");
    append_bang(&mut owned);
    append_bang(&mut owned);
    println!("{}", owned);
}

&mut requires the underlying variable to be declared mut. The function declares it takes a &mut String. The caller passes &mut owned. Three places that have to agree — and the compiler enforces all three.

4 · The famous rule — aliasing XOR mutation

rust src/main.rs · won't compile
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;       // shared borrow
    let r2 = &s;       // another shared — fine
    let r3 = &mut s;   // ERROR: cannot borrow s as mutable because it is also borrowed as immutable

    println!("{} {} {}", r1, r2, r3);
}

You may have many shared references at the same time. Or one mutable reference. You may not have both. This is the rule that catches every data race at compile time — and the rule that the first month of Rust feels like a fight with the compiler about.

Non-lexical lifetimes. Since 2018, the compiler is smart enough to see that a borrow's actual usage may end before the variable goes out of scope. So let r1 = &s; println!("{}", r1); let r2 = &mut s; compiles — r1 isn't used after the print.

5 · Common patterns

rust src/main.rs · pass-by-ref or pass-by-mut-ref
// Read-only — take &T
fn sum(xs: &Vec<i32>) -> i32 {
    xs.iter().sum()
}

// Modify in place — take &mut T
fn push_double(xs: &mut Vec<i32>, v: i32) {
    xs.push(v * 2);
}

fn main() {
    let mut xs = vec![1, 2, 3];
    println!("sum = {}", sum(&xs));
    push_double(&mut xs, 5);
    println!("xs = {:?}", xs);
    println!("sum = {}", sum(&xs));
}

6 · Common mistakes

  • Borrowing mutably while still using a shared borrow. The compiler is precise about overlap; restructure so they don't overlap, or drop one explicitly.
  • Mixing &T and &mut T on the same struct field. Sometimes the fix is splitting into two structs that the compiler can borrow independently.
  • Returning a reference to a local. Won't compile — lifetimes (next concept). Return owned or take a reference to extend the caller's data.
  • Forgetting that self is a borrow. Methods take &self (read), &mut self (write), or self (consume). The same rules apply.

7 · When it clicks

  • Function signatures tell you the access pattern at a glance.
  • You reach for &T by default, &mut T only when you need to mutate.
  • The "cannot borrow as mutable while immutable borrow is in use" error becomes a useful hint rather than a frustration.
  • You stop reaching for .clone() as a borrow-checker fix.
Found this useful?