The Three Rules, Stated Once and Applied Everywhere
Ownership is the first thing interviewers ask about Rust and the thing every Rust program obeys. The entire model is three rules:
- Each value in Rust has exactly one variable that is called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (its memory is freed).
That is the whole model. The compiler enforces it statically, so Rust frees memory without a garbage collector and without free calls — freeing is tied to scope, and scope is a lexical property the compiler can see.
Watch the rules run:
fn main() {
let s1 = String::from("hello"); // s1 owns the String
let s2 = s1; // ownership MOVES to s2; s1 is no longer valid
println!("{}", s2); // works: prints "hello"
// println!("{}", s1); // error[E0382]: borrow of moved value: s1
} // s2 drops here — memory freed exactly once
String moves because it owns heap memory. A second owner would mean two chances to free the same allocation — the double-free bug C programmers know well. Rust's answer: the assignment transfers ownership, it does not copy. s1 is dead at that line, and the compiler will refuse any later use of it, naming both the move and the attempted borrow in its error.
Not every type moves. i32, f64, bool, and char — types whose data is fully inline — implement the Copy trait, and their assignment duplicates the bits instead (that distinction is Copy and Clone Traits). So the rules apply to every value, but the visible effect differs: copying types keep both owners alive as duplicates; move types transfer.
The mental model that scales: think of each binding as holding a deed to a value. let writes the deed; assignment rewrites the deed to a new name; a function parameter's let rewrites it again. The value itself never moves around for heap data — the deed does. When you later read E0382: borrow of moved value, the compiler is telling you the deed changed hands and you are reading a stale name. This framing also explains borrowing (the next phase): a borrow is a temporary lease that does not touch the deed. State the three rules cleanly in an interview, then immediately demonstrate them on a String — that sequence answers the question better than any analogy about houses.
Here is the diagnostic in full, because this exact output is what an interviewer will hand you on a whiteboard:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:6:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
...
6 | println!("{}", s1);
| ^^^ value borrowed here after move
Three things to read out loud: the reason clause names the type and the missing Copy trait, the second span points at the line that took ownership, and the third span is the use that broke. Modern rustc phrases it as "borrow of moved value" because println! takes &s1 — the move is what made the borrow illegal. The suggested repairs it offers are the topic's whole agenda: borrow instead of move (&s1), clone explicitly (s1.clone()), or restructure so the value is never moved out.
The obvious follow-up question is "what if I genuinely need two owners?" The model has an answer rather than a loophole: Rc<String> wraps the value in a reference-counted allocation so the wrapper can be cloned cheaply while the value keeps exactly one owner — the count, not the bindings, decides when it drops. Ownership stays singular; only the bookkeeping multiplies. That is the design posture to name in an interview: Rust does not relax rule 2, it moves the sharing into a type whose cost and thread-safety limits are explicit (Rc is single-threaded, Arc is the atomic version).
Scope Is the Drop Trigger: Tracing Who Frees What
Rule 3 — "when the owner goes out of scope, the value is dropped" — is what turns the ownership model into automatic memory management. Scopes are lexical blocks, and the compiler tracks each owner's scope precisely:
fn main() {
let outer = String::from("outer");
{
let inner = String::from("inner");
println!("{}", inner);
} // inner drops HERE — its block ended
// println!("{}", inner); // error[E0425]: cannot find value `inner`
println!("{}", outer);
} // outer drops at the end of main
Dropping runs in reverse declaration order — inner before outer — so an earlier value that a later value borrowed from stays alive as long as the borrow needs it. This ordering is a guarantee, not an accident; it is what makes it safe to hold references into your own locals.
Move changes which scope frees the value:
fn take(input: String) -> String {
input.len(); // input owns the String now
input // ownership returned to the caller via the return value
}
fn main() {
let s = String::from("hello");
let s = take(s); // caller's s moved into `input`, then moved back out
println!("{}", s); // fine — exactly one owner (the new s) at every moment
} // one drop, at the end of main
Inside take, the local input is the sole owner; when take returns it, the value is moved to the caller's binding and take's scope end drops nothing. If take had no -> String return and the caller discarded the argument, the value would drop at the end of take — this is why passing a String to a function and trying to use it afterward fails: the callee's scope, not yours, would free it.
Two refinements that matter in real code. First, borrows do not own — &s creates a temporary reference that neither keeps the value alive nor gets to drop it; the original owner (and only it) frees the value. Second, the drop happens at scope end but the code that runs is well-defined: for types with a Drop impl, destructor code runs; then the memory is released. For plain data like i32, "dropping" is a no-op — the compiler just reclaims the stack slot. Ownership, then, is not only about heap memory: it is the general rule for who is responsible for cleanup, and it is uniform across String, files, sockets, and locks. Deterministic destruction — cleanup at a known line, not at some collector's whim — is also why RAII-style resource management works in Rust the way it never quite did in garbage-collected languages.
Rule 3 also governs values you never named. A temporary exists until the end of the enclosing statement, so this guard is released at the semicolon, not at the closing brace:
let total = orders.iter().map(|o| o.qty).sum::<u64>();
// every String::from, every Vec built inside the chain,
// and any MutexGuard temporaries drop when this line ends
That is why let n = mutex.lock().unwrap().count(); is safe — the lock guard is a temporary in the expression, and its scope is the statement. Holding the lock longer requires binding it to a name (let guard = mutex.lock().unwrap();), which changes the owner's scope and therefore the drop point. Scope, not time, decides; and you can choose the scope:
{
let cache = load_cache(); // heavy, only needed here
probe(&cache);
} // cache drops before the next phase
An inner block is the idiomatic way to give a value a shorter life — the same tool you will use to end a borrow early once mutable references enter the picture.
If you ever need proof of what dropped when, write it: implement Drop on a wrapper, print in drop, and run the program. The output order is reverse declaration order, it is deterministic across runs, and it survives moves (a value moved out of a into b prints once, from b). When it prints once and at the line you predicted, rule 3 has stopped being an abstraction and become an observable property of your binary.
Drop: Deterministic Cleanup You Can Write Yourself
Rule 3 says values are dropped at scope end. The Drop trait is where that becomes user-defined behavior:
struct DbConnection {
name: String,
}
impl Drop for DbConnection {
fn drop(&mut self) {
println!("closing connection to {}", self.name);
// real code: flush buffers, send TCP FIN, release file descriptor
}
}
fn main() {
let conn = DbConnection { name: String::from("prod") };
println!("querying...");
} // prints "closing connection to prod" — automatically, exactly once
This is RAII (resource acquisition is initialization): acquire in the constructor, release in the destructor, and the borrow checker's scope tracking guarantees the destructor runs. A File wrapper closes its descriptor, a MutexGuard unlocks its mutex, an Arc decrements a reference count — all through this one trait, all triggered by rule 3.
Three Drop mechanics are interview-grade facts. First, drop order is reverse declaration order within a scope:
fn main() {
let a = DbConnection { name: String::from("first") };
let b = DbConnection { name: String::from("second") };
} // prints "second" then "first" — b drops before a
Second, you cannot call the destructor manually by method call: conn.drop(); is a compile error (error[E0040]: explicit destructor call not allowed) because it would run cleanup and then let scope-end run it again — a double free by another name. To end a value early, use the free function std::mem::drop(conn), which moves the value in and lets the compiler drop it at the end of that statement:
let guard = mutex.lock().unwrap();
std::mem::drop(guard); // lock released HERE, not at scope end
mutex.lock().unwrap(); // no deadlock
Third, and subtle: a type that implements Drop cannot also implement Copy (the compiler rejects it with E0184). Copy duplicates values bit-by-bit; duplicates of a Drop type would each run the destructor — freeing the same resource once per copy. That single incompatibility is the hinge between this topic and Copy and Clone Traits coming next; it is also the compiler's way of encoding "you cannot both duplicate a resource and clean it up exactly once."
Why all of this beats a garbage collector in interviews: cleanup timing is deterministic and local. A GC frees memory eventually — unbounded delay, no ordering guarantees between objects, and no way to release a file descriptor when a variable expires (it frees memory, not OS resources). Rust's scope-driven Drop runs at a line you can point to, in a defined order, which is exactly what non-memory resources require.
Two edge cases separate people who have written Drop from people who have only read about it.
First, the order inside a composite: when a struct is dropped, its Drop::drop method runs first, and then its fields drop in declaration order — the opposite direction from local variables in a block, which drop in reverse declaration order. Writing a destructor that reads a field is therefore safe by construction:
impl Drop for DbConnection {
fn drop(&mut self) {
self.flush(); // self.name and self.sock are still alive here
}
} // only AFTER this returns do the fields drop
Second, opting out. Rust lets you leak on purpose, which is the honest answer to "can Rust guarantee no leaks?":
use std::mem::{forget, ManuallyDrop};
let leaky = Box::new(vec![1, 2, 3]);
let leaked: &'static mut Vec<i32> = Box::leak(leaky); // never dropped, usable 'static
forget(important_log); // skipped entirely — safe, just leaked
let quiet = ManuallyDrop::new(expensive); // same, but the value stays usable
None of this is unsafe and none of it is a bug: memory safety means a leak cannot cause a dangling pointer or a double free, only wasted memory. What Rust actually guarantees is the negative — use-after-free and double-free are unrepresentable — while leaks, including reference cycles in Rc graphs, remain possible and must be broken with Weak. Deterministic Drop makes leaks rare and debuggable; it does not, and does not claim to, make them impossible.
Quiz
1. Which is the correct statement of Rust's ownership rules?
2. In `let s1 = String::from("hi"); let s2 = s1; println!("{}", s1);`, what happens and why?
3. Why does `conn.drop();` fail to compile while `std::mem::drop(conn);` works?
4. What does implementing Drop for a struct give you, and what does it NOT guarantee?
Flashcards
Question
State Rust's three ownership rules.
Click to reveal answer
Answer
1) Each value has exactly one owner. 2) There can be only one owner at a time — assignment transfers ownership (a move) rather than copying. 3) When the owner goes out of scope, the value is dropped (memory freed / cleanup run). The compiler enforces all three statically.
Question
Why does `let s2 = s1;` invalidate s1 for String but not for i32?
Click to reveal answer
Answer
String owns heap memory and is not Copy, so the assignment MOVES ownership to s2 and s1 is dead (E0382 on reuse). i32 implements Copy — it is fully inline data with no resource to free — so the assignment duplicates the bits and both bindings stay valid, each owning its own copy.
Question
In what order are values dropped within a scope, and why does the order matter?
Click to reveal answer
Answer
Reverse declaration order — the most recently declared value drops first. It matters because an earlier value may be borrowed by a later one; dropping borrowers before the borrowed-from value keeps every reference valid until its scope ends. The order is a documented guarantee, so destructors can rely on it.
Question
What is the Drop trait, and how is it different from a garbage collector?
Click to reveal answer
Answer
Drop lets a type define a destructor (fn drop(&mut self)) that runs exactly once when the owner goes out of scope — RAII cleanup for files, locks, connections. Unlike a GC, timing is deterministic and local: cleanup happens at a known line in reverse declaration order, and it frees resources (not just memory) at the moment they are no longer owned.
Question
Why can a type not implement both Copy and Drop? (and what error proves it?)
Click to reveal answer
Answer
Copy duplicates values bit-by-bit on assignment; a Copy+Drop type would run its destructor once per duplicate — freeing the same resource N times (double/multi free). Rust forbids it outright: error[E0184] 'cannot have both `impl Copy` and a manual `Drop` impl'. Copy is only for types with nothing resource-like to clean up.
Revision Notes
Key Takeaways
- 1. Three rules — one owner, one owner at a time, drop at scope end — are the entire ownership model; everything else (moves, borrows, lifetimes) derives from them
- 2. Assignment of a non-Copy type MOVES the deed, not the data: the old binding dies (E0382) and exactly one owner remains to free the value
- 3. Drops run in reverse declaration order at scope end — a guarantee that keeps borrowed-from values alive longer than their borrowers
- 4. Drop is RAII: user-defined deterministic cleanup for any resource, callable only via mem::drop (which moves) because a direct .drop() call would double-free
- 5. Copy types (integers, etc.) opt out of moving by duplicating bits — and are barred from implementing Drop, since duplication and single cleanup are incompatible
Interview Tips
- • "State the three ownership rules" — say them precisely, then immediately show a String move and the E0382 error; a clean recitation plus a live trace is the strongest possible answer
- • "How does Rust free memory without a GC?" — scope-driven drop: the compiler knows each owner's scope statically and inserts the free/destructor at scope end (reverse declaration order); no runtime, no pause, no leaks
- • "Why can't you call .drop() directly?" — it would run cleanup while the binding still owns the value, and scope end would drop it again (double free); mem::drop consumes the value so cleanup happens exactly once
- • "Copy vs move, in one sentence" — Copy types duplicate their bits (no resource to free, both bindings valid); everything else moves, invalidating the old owner at compile time — and Copy+Drop is impossible (E0184)
Cheat Sheet
The Three Ownership Rules
- Each value has exactly one owner.
- One owner at a time — assignment transfers ownership (a move).
- Owner out of scope → value dropped (freed / destructor runs).
Trace:
let s1 = String::from("hi"); // s1 owns
let s2 = s1; // MOVE to s2; s1 invalid (E0382 if reused)
let s3 = 42_i32; // Copy type: assignment duplicates bits, no move
Drop mechanics:
| Fact | Detail |
|---|---|
| Trigger | owner's scope end (lexical, compiler-tracked) |
| Order | reverse declaration order (guaranteed) |
| Manual call | conn.drop() = E0040 (would double-free); use std::mem::drop(conn) which MOVES it in |
| Early release | mem::drop(guard) to release a lock/file before scope end |
| Copy+Drop | forbidden — E0184; duplicates would each run the destructor |
RAII: acquire in constructor, release in Drop — File closes fd, MutexGuard unlocks, Arc decrements refcount. Deterministic, ordered, resource-aware — unlike a GC's eventual memory-only sweep.