04 / 20 · Day 2
Day 2 · Concept 04

Ownership

The single idea that makes Rust safe without a garbage collector. Every value has exactly one owner. Assignment moves ownership; the source becomes unusable. When the owner leaves scope, the value is dropped — destructors run, memory is freed. The compiler tracks all of it at compile time.


1 · The three rules

  1. Each value has a single variable that's its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped.

Three sentences. The entire memory model rests on them. The compiler refuses any program that violates these rules — and that refusal is what eliminates use- after-free, double-free, and most data races.

2 · Try it — move semantics

rust src/main.rs · the move that catches everyone
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;            // ownership MOVES from s1 to s2

    // println!("{}", s1);  // ERROR: value borrowed after move
    println!("{}", s2);     // OK
}

After let s2 = s1, s1 is no longer valid. The heap-allocated String has a new owner. This is different from C++ (which copies), JavaScript (which references-share), or Go (which copies the header). Rust moves.

Why move? A String owns a heap buffer. If two variables both "owned" it, who frees it? Move semantics make ownership unique; the compiler can statically place the drop at exactly one point.

3 · The Copy trick — small types skip the move

rust src/main.rs · integers Copy, Strings move
fn main() {
    // i32 is Copy — assignment copies the bits, no move.
    let a: i32 = 5;
    let b = a;
    println!("{} {}", a, b);  // both valid

    // Vec is NOT Copy — moves on assignment.
    let v = vec![1, 2, 3];
    let w = v;
    // println!("{:?}", v); // ERROR
    println!("{:?}", w);
}

Types that are cheap and don't own heap data implement the Copy trait — integers, floats, booleans, chars, fixed-size tuples of Copy types. Assignment copies the bits, no move. Types that own heap memory (String, Vec, HashMap, anything with a Drop implementation) cannot be Copy and always move.

4 · Functions move arguments by default

rust src/main.rs · move into function
fn take(s: String) {
    println!("taking: {}", s);
    // s is dropped here when the function ends
}

fn main() {
    let owned = String::from("data");
    take(owned);
    // println!("{}", owned); // ERROR: moved into take
}

Calling take(owned) transfers ownership of owned into the function's parameter. After the call, owned is invalid in main. To use the value again afterwards, either return it (taking it back) or pass a reference with & (next concept).

5 · Returning ownership

rust src/main.rs · ping-pong ownership
fn add_excitement(mut s: String) -> String {
    s.push('!');
    s   // ownership returned to caller
}

fn main() {
    let greeting = String::from("hi");
    let louder = add_excitement(greeting);
    // greeting is gone; louder owns the upgraded String
    println!("{}", louder);
}

6 · Clone — opt-in deep copy

rust src/main.rs · clone when you need both
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();    // deep copy — both valid; both own their data
    println!("{} {}", s1, s2);
}
When to clone. When you need two independent copies of heap data. Not as a "fix" for the borrow checker. Cloning is explicit and visible in the source — it announces a cost. If you find yourself cloning often, the design probably wants references (next concept).

7 · Drop — what happens at scope exit

rust src/main.rs · custom Drop
struct File { name: String }

impl Drop for File {
    fn drop(&mut self) {
        println!("closing file: {}", self.name);
    }
}

fn main() {
    let _f = File { name: String::from("hello.txt") };
    println!("main running");
    // _f.drop() called automatically here
}
// Output:
//   main running
//   closing file: hello.txt

The Drop trait runs automatically at the end of the owner's scope. This is how Rust handles RAII without a destructor keyword. File handles, mutex guards, allocations — all release automatically.

8 · Common mistakes

  • "Use after move" compile errors. Pass a reference (&value) or call .clone(). Don't reach for .clone() first — try references first.
  • Cloning to "shut the compiler up". A smell. The compiler probably wanted you to use a reference; clone defers the cost rather than removing it.
  • Returning a reference to a local. Won't compile (next concept: lifetimes). Return owned data, or take a reference to extend a caller's data.
  • Storing closures that capture by move when you wanted reference. Use move || when you want move, plain || when you want the inferred mode (often by-reference).
  • Forgetting that primitives are Copy. let a = 1; let b = a; doesn't move — a is still valid. Different rule than for String.

9 · Exercises (~15 min)

  1. Trigger the error. Write code that moves a String and then tries to use the original. Read the compile error carefully — it's instructive.
  2. Pass and return. Write a function that takes a Vec<i32>, pushes 99 to it, and returns it. Use it in main — note the value comes back.
  3. Custom Drop. Build a Connection struct that prints "open" on creation and "close" in Drop. Create one in a block; verify it closes at the closing }.
  4. Clone audit. Take any .clone() call you've written today. Ask: could a reference (&T) replace it? Probably yes.

10 · When it clicks

  • You stop reaching for .clone() as a first response.
  • Function signatures tell you the ownership story — pass T to give up; pass &T to lend.
  • Drop becomes an asset, not magic. File closes, mutex releases, allocations free — all without you writing it.
  • You notice the compiler's move-error message is precise and helpful, not adversarial.
Found this useful?