Rustlers Atom 2.1: Rust ownership explained

Ownership is Rust’s replacement for the garbage collector, manual free, and “hope nothing double-frees” all at once. It’s a compile-time bookkeeping system with one simple law:

Every value has exactly one owner. When the owner goes out of scope, the value is dropped.

That’s it. Everything else is consequences.

Scopes and owners

Start with the smallest example. Here x owns the value 42. When the inner block ends, x goes out of scope and its storage is released.

fn main() {
    {
        let x = 42;
        // x is valid here
        println!("x = {}", x);
    }
    // x is out of scope here, memory for x is gone
}

Moves: passing the baton

Ownership doesn’t stay glued to one variable, it can move.

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

    // println!("{}", s1); // ❌ compile error: value used after move
    println!("{}", s2);   // ✅ ok
}

What happened?

  • The value (the heap allocation and its bookkeeping) is the same.
  • The owner changed: s1s2.
  • After the move, s1 is considered invalid by the compiler. You can’t use it anymore.

At runtime, this is just a cheap pointer+length copy. At compile time, Rust marks s1 as moved so you don’t accidentally use a pointer that would later be freed twice.

The same move happens on function calls and returns:

fn main() {
    let v = vec![1, 2, 3];
    consume(v);   // move into function
    // println!("{:?}", v); // ❌ v moved

    let w = make_vec();   // move out of function
    println!("{:?}", w);  // ✅ owns the returned Vec
}

fn consume(numbers: Vec<i32>) {
    println!("{:?}", numbers);
} // numbers dropped here

fn make_vec() -> Vec<i32> {
    vec![10, 20, 30]
} // move to caller instead of dropping

Reading rule-of-thumb:

  • Assigning an owned value: let b = a;a is moved into b.
  • Passing an owned value as an argument → moved into the parameter.
  • Returning an owned value → moved back to the caller.

Copy behavior and why Strings don’t do it

Some values don’t move the way String does. Here x is still usable after let y = x;. Integers behave like they did in C: cheap, copied values.

fn main() {
    let x = 5;
    let y = x;      // looks like a copy
    println!("{x}, {y}"); // both are fine
}

For String, Vec<T>, and otherowning containers, let b = a; is a move. For simple scalar types (i32, bool, char, etc.), let b = a; is a copy. Both are usable. We’ll formalize this with the Copy trait in the next atom.

The language forces you to be explicit when you really want a second, independent owned value. That .clone() call is a big neon “this might be expensive” sign.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // explicit clone
    println!("{s1}, {s2}");
}

You'll only receive email when they publish something new.

More from GSLF
All posts