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
- Each value has a single variable that's its owner.
- There can only be one owner at a time.
- 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
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.
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
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
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
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
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — both valid; both own their data
println!("{} {}", s1, s2);
}7 · Drop — what happens at scope exit
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.txtThe 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 —ais still valid. Different rule than forString.
9 · Exercises (~15 min)
- Trigger the error. Write code that moves a
Stringand then tries to use the original. Read the compile error carefully — it's instructive. - 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. - Custom Drop. Build a
Connectionstruct that prints "open" on creation and "close" in Drop. Create one in a block; verify it closes at the closing}. - 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
Tto give up; pass&Tto 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.