07 — Ownership (The Heart of Rust)
Ownership is the defining feature of Rust. Every other memory-safety guarantee flows from these rules.
The Three Rules
- Each value has exactly one owner (a variable).
- When the owner goes out of scope, the value is dropped (its destructor runs).
- Assigning or passing a value moves it (for non-
Copytypes) — the old binding becomes invalid.
Stack vs Heap
- Stack: fast, LIFO, fixed-size values (integers,
bool, fixed arrays, pointers). - Heap: dynamic, slower, runtime-allocated (
Box,Vec,String). Ownership primarily concerns heap data.
let s1 = String::from("hi"); // heap allocation
let s2 = s1; // MOVE — s1 is now invalid
// println!("{s1}"); // ERROR: borrow of moved value
String is { ptr, len, capacity } (stack) pointing to heap bytes. A move copies the stack header and invalidates the old binding so you can't have two owners trying to free the same heap memory.
The Copy Trait
Types whose bits can be trivially copied without invalidating the source are Copy:
- All integer/float/bool/char types.
- Tuples/arrays of
Copytypes. &T(shared references areCopy).- Function pointers
fn(...).
Non-Copy types (heap-ish): String, Vec, Box, HashMap, any type with a destructor or that owns a resource.
let a = 5;
let b = a; // i32 is Copy — a is still valid
println!("{a} {b}"); // fine
Move Semantics in Functions
fn take(s: String) { println!("{s}"); }
let s = String::from("hi");
take(s);
// s is now invalid — moved into the function
To keep ownership, pass by reference or clone():
take(s.clone()); // s still owned here
take(&s); // pass reference (covered in References chapter)
Returning Ownership
fn make() -> String { String::from("hi") }
let s = make(); // ownership moves to caller
Returning transfers ownership out without a copy. This is the Rust idiom for "constructing" data.
Drop Order
Destructors run in reverse declaration order within a scope:
{
let a = String::from("a");
let b = String::from("b");
// b drops, then a drops
}
Drop trait's drop(&mut self) is the destructor. You usually don't call it manually — use std::mem::drop(value) to drop early.
Drop and Copy are Mutually Exclusive
A type with a custom Drop cannot be Copy (you can't derive both). Copy means "duplicate bits"; Drop means "do something on cleanup" — duplicating would risk double-cleanup.
Partial Moves
struct Person { name: String, age: u32 }
let p = Person { name: "Ada".into(), age: 36 };
let n = p.name; // partial move — p.name is moved, p.age still valid
// println!("{}", p); // ERROR: p partially moved
println!("{}", p.age); // OK — only name was moved
You can still access non-moved fields after a partial move.
Move Footguns
- Closure captures:
|| use_s(s)movessinto the closure ifsis consumed inside. Vec/Stringinmatcharms: moving a value out in one arm invalidates it in others; the compiler ensures all paths move or none do.- Field reorder / re-init: after a partial move, you can reassign the moved field (
p.name = "Bob".into();) to makepwhole again. mutbinding of a moved value:let mut s = String::new(); let t = s; s = String::from("x");— re-binding is fine;swas invalid between the move and reassignment.
drop Order in Structs
Struct fields drop in declaration order (NOT reverse), per RFC 1857. This is a common surprise:
struct A { /* ... */ }
impl Drop for A { fn drop(&mut self) { println!("A dropped"); } }
struct Pair { first: A, second: A }
// when a Pair is dropped: first drops, then second
Tuple fields drop in order 0, 1, 2, ...
ManuallyDrop and MaybeUninit
For unsafe manual memory management, use std::mem::ManuallyDrop to prevent auto-drop, or std::mem::MaybeUninit for uninitialized memory. These are advanced; covered in the Unsafe chapter.
Why Ownership Is Unique
Languages choose between:
- GC (Java, Go, Python): runtime cost, pause times.
- Manual management (C, C++): use-after-free, double-free, leaks.
- Ownership (Rust): compile-time rules, zero runtime cost, but you learn the borrow checker.
Common Error: cannot move out of ...
let v = vec![String::from("a"), String::from("b")];
let first = v[0]; // ERROR: cannot move out of index of Vec
Indexing returns a reference (&String); moving out would leave the Vec in an invalid state. Use v.into_iter().next() or mem::take(&mut v[0]) or v.remove(0).
mem::take and mem::replace
use std::mem;
let mut s = String::from("hi");
let taken = mem::take(&mut s); // s becomes default (empty String), taken gets "hi"
let prev = mem::replace(&mut s, "bye".into()); // s = "bye", prev = ""
These let you extract values from behind a mutable reference without invalidating the container.
Summary
- Each value has one owner; scope-end drops it.
- Non-
Copytypes move on assignment/pass;Copytypes duplicate. Dropis a destructor; can't be combined withCopy.- Partial moves,
mem::take,mem::replacelet you surgically move things around.
Next: References and Borrowing — using a value without owning it.