7.6 The Rules of Ownership: Three Simple Laws
Alright, let’s get down to brass tacks. You’ve heard the term “Ownership” whispered in hushed, reverent tones by Rust developers. It sounds mystical, maybe even a little intimidating. It’s not. It’s just three brutally simple rules that the compiler enforces with the zeal of a bouncer at an exclusive club. Master these, and you’ve mastered the core of Rust’s memory safety guarantees without a garbage collector in sight. Let’s meet our new overlords.
First, every value in Rust has a single owner. This is the variable it’s bound to. Think of it like a deed to a house. You can only have one official deed. When that variable goes out of scope, the value is dropped—its memory is freed. This is Rust’s secret sauce: deterministic cleanup.
{
let owner = String::from("This is my string"); // The String is owned by `owner`
// do stuff...
} // <- `owner` goes out of scope here. `drop` is called, and the memory is freed.
// Poof. Gone. No garbage collection needed.
Second, you can only have one active mutable reference or any number of immutable references to a value at a time. This is the concurrency police showing up to prevent data races at compile time. It’s genius, and honestly, it’s the rule that causes the most initial head-banging. The compiler is stopping you from shooting yourself in the foot before you even write the multithreaded code.
let mut message = String::from("hello");
let ref1 = &message; // First immutable borrow? All good.
let ref2 = &message; // Second immutable borrow? Still fine.
println!("{} and {}", ref1, ref2); // We're using them here.
// Now, after the last use of the immutable borrows...
let mutable_ref = &mut message; // This is now allowed. The compiler sees the others are done.
mutable_ref.push_str(", world!");
// println!("{}", ref1); // This would be a COMPILE ERROR. The mutable borrow invalidated the immutable ones.
Why this draconian rule? Imagine ref1 is reading the string while mutable_ref is changing it. What does ref1 see? A mess. A data race. Undefined behavior. Rust just says “nope” and makes you structure your code so this ambiguity is impossible.
Third, and this follows from the first: when you assign a value to another variable or pass it to a function, you’re moving it. The original owner loses the deed. It’s invalidated. This prevents double-frees. You can’t free the same memory twice if only one variable ever has ownership at a time.
let first_owner = String::from("I own this");
let second_owner = first_owner; // The value is MOVED. first_owner is no longer valid.
// println!("{}", first_owner); // COMPILE ERROR: borrow of moved value: `first_owner`
println!("{}", second_owner); // This is perfectly fine.
This is where new Rustaceans get tripped up. “I just wanted to copy it!” you’ll yell. For types that are cheap to copy and stored entirely on the stack (like integers, booleans, chars), Rust does just copy them. These types implement the Copy trait. But for anything that manages heap data (like String, Vec), a move is the default. It’s efficient; it doesn’t do a deep copy. If you want a deep copy, you have to explicitly ask for it with .clone().
let cheap_to_copy = 42;
let a_copy = cheap_to_copy; // This is copied because i32 implements `Copy`.
println!("Original: {}, Copy: {}", cheap_to_copy, a_copy); // Both are fine.
let expensive_to_copy = String::from("hello");
let an_explicit_clone = expensive_to_copy.clone(); // This allocates new memory on the heap.
println!("Original: {}, Clone: {}", expensive_to_copy, an_explicit_clone); // Both work.
The Borrow Checker: Your New Best Frenemy
The entity that enforces these rules is called the borrow checker. It lives in the compiler and it is thorough. You will fight with it. You will call it names. And then you will realize it’s always right and is saving you from yourself. Its job is to track the lifetime (the scope for which a reference is valid) of every borrow and ensure the rules are never broken. The error messages are famously excellent, guiding you toward the correct, safe code.
Common Pitfalls and Best Practices
The most common initial struggle is with function calls. Passing a value into a function moves ownership into the function. Unless the function returns it back, it’s gone.
fn take_ownership(s: String) { // s comes into scope
println!("{}", s);
} // s goes out of scope and is dropped. Goodbye.
fn main() {
let my_string = String::from("hello");
take_ownership(my_string); // MOVED into the function
// println!("{}", my_string); // COMPILE ERROR: value borrowed after move
}
The solution? If you just need to look at the data, lend it to the function with a reference (&my_string) instead of giving it away. Or return it from the function if you truly need to transfer ownership.
These three rules—ownership, borrowing, and lifetimes—form a cohesive, elegant system. It feels restrictive at first because you’re used to the chaotic freedom of other languages. But that freedom comes at a cost: runtime errors, memory leaks, and data races. Rust trades a little upfront friction for a staggering amount of runtime confidence. Trust the system. It works.