Skip to content
Borrowing — & and &mut
step 1/7

Reading — step 1 of 7

Learn

~3 min readOwnership Patterns

Borrowing — & and &mut

Rust's ownership system is three rules:

  1. Every value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (freed) — no garbage collector needed.
  3. Instead of transferring ownership, you can borrow: &T to read, &mut T to modify.

The owner is whoever is responsible for freeing the memory. A borrow is a temporary permission slip: "you may look" (&) or "you may modify — and nobody else may even look while you do" (&mut).

Moves: assignment transfers ownership

For heap-owning types like String and Vec, assignment doesn't copy — it moves:

let s1 = String::from("hi");
let s2 = s1;                 // ownership moves to s2
println!("{}", s1);          // trap: s1 is gone
error[E0382]: borrow of moved value: `s1`

Why so strict? If s1 and s2 both owned the same heap buffer, both would free it when dropped — a double free. Rust invalidates s1 at compile time instead.

  • Need a second independent copy? .clone() — an explicit, visible heap allocation.
  • Small stack-only types (i32, f64, bool, char, and tuples/arrays of such types) are Copy: assignment copies, nothing moves. Your own structs are not Copy automatically — you must opt in with #[derive(Copy, Clone)], and only if every field is itself Copy.

Borrowing: lend, don't move

Usually a function just needs to use a value, not own it:

fn print_len(s: &String) {          // shared borrow — read-only
    println!("{}", s.len());
}

fn append_world(s: &mut String) {   // exclusive borrow — may mutate
    s.push_str(", world");
}

fn main() {
    let mut name = String::from("hello");
    print_len(&name);               // lend read access
    append_world(&mut name);        // lend write access
    println!("{}", name);           // hello, world — main still owns it
}

append_world changes the caller's String in place. No return value needed: the mutation happens through the &mut reference.

The XOR rule

At any moment a value may have either any number of &T or exactly one &mut T — never both:

let mut v = vec![1, 2, 3];
let first = &v[0];    // shared borrow still alive...
v.push(4);            // ...while we mutate
println!("{}", first);
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

This looks pedantic until you see why: push can reallocate the vector's buffer, leaving first pointing at freed memory. The XOR rule makes that use-after-free unrepresentable — and the same rule, applied across threads, is why safe Rust has no data races.

One more trap: to lend &mut, the binding itself must be mut. Writing let s = ...; and then f(&mut s) fails with error[E0596]: cannot borrow s as mutable, as it is not declared as mutable.

Your exercise

Define fn append_excl(s: &mut String) that appends ! — one line: s.push('!'); (or s.push_str("!")). Then uncomment the call in main. The starter already reads one line, trims the trailing newline, and prints the string.

TEST 1 feeds hello and expects exactly:

hello!

Mistakes the grader catches:

  • Leaving the call commented out — compiles fine, prints hello, fails (expected hello!).
  • Taking s: String by value — the call site passes &mut s, so you get error[E0308]: mismatched types.
  • Taking s: &String — the call compiles (&mut coerces to &), but the mutation inside fails: error[E0596]: cannot borrow *s as mutable, as it is behind a & reference. Only &mut String grants write access.
  • Returning a new String — the function returns (). Mutate through the reference; the caller's s changes in place.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…