Ownership & Borrowing

Ownership is the idea that lets Rust be memory-safe with no garbage collector. Every value has a single owner; ownership can be moved or borrowed, and the borrow checker enforces the rules at compile time.

The Ownership Rules

Three rules drive everything. Memorize them — every borrow-checker error traces back to one of these:

  1. Each value in Rust has a single owner.
  2. When the owner goes out of scope, the value is dropped (its memory freed).
  3. A value can be owned by exactly one binding at a time; moving it away invalidates the old binding.
fn main() {
    // the String is owned by `s`; when `main` ends it is freed automatically
    let s = String::from("hello");
    println!("{s}");
}   // `s` drops here — no free(), no GC, no leak

Move Semantics

Assigning an owned value transfers ownership. The original binding is then unusable — using it afterwards is a compile error, not undefined behavior:

Move semantics: s1 hands its String value to s2, s1 becomes invalid

Figure 1 — assigning s1 to s2 moves the value: s1 is invalidated and s2 owns it.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;                 // ownership moves to s2

    // println!("{s1}");         // ERROR: s1 was moved and is now invalid
    println!("{s2}");            // ok
}

Types whose values are cheap to copy bit-for-bit implement Copy (integers, floats, bool, char, and tuples of them). Those simply copy on assignment instead of moving:

fn main() {
    let x = 5;
    let y = x;            // i32 is Copy: x stays valid
    println!("{x} {y}");  // both work — no move happens
}

Borrowing

Using a value without taking ownership means borrowing it. A shared borrow &T lets many readers look at once; an exclusive borrow &mut T lets one writer change it. The borrow checker enforces "many readers or one writer":

Borrowing: many shared &T readers or one &mut T writer, never both

Figure 2 — shared reads may coexist; an exclusive write excludes all others.

fn main() {
    let mut s = String::from("hello");

    let len = str_len(&s);          // shared borrow: read only
    println!("length = {len}");

    append_world(&mut s);           // exclusive borrow: may mutate
    println!("{s}");
}

fn str_len(value: &String) -> usize { value.len() }          // &T
fn append_world(value: &mut String) { value.push_str(" world"); } // &mut T

Mutable Borrowing & Aliasing

While an exclusive borrow is alive, no other borrow of the same value may exist. This rules out data races and iterator-invalidation bugs before the program runs:

fn main() {
    let mut s = String::from("rust");

    {
        let r = &mut s;        // one exclusive borrow
        r.push('!');
    }                          // borrow ends here

    let r1 = &s;               // now many shared borrows are fine
    let r2 = &s;
    println!("{r1} {r2}");
}

Slices

A slice (&str, &[T]) is a borrowed view of a contiguous range. It owns nothing, so functions can work on part of a value while the borrow rules stay simple:

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);      // &str slice, borrows s
    println!("first word: {word}");
}

fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}
Practice move: in the lab examples, add a println!("{s1}") after a move and read the error — the compiler points at the exact move site.