Rust, part 1: ownership, borrowing, and the cost model that replaces a GC
Part 1from the Rust series · 15 parts in all
Every language makes you pay for memory management somewhere. Rust's wager is that you can pay at compile time instead of at runtime, and the price is a rule set small enough to state in three sentences. If you have written Java, C#, Go or Python, the surprise is not that Rust is strict — it is that after a week the strictness stops feeling like a tax.
The three rules
- Every value has exactly one owner.
- When the owner goes out of scope, the value is dropped — freed, file closed, lock released.
- You may have many immutable references to a value, or one mutable reference, never both at once.
fn main() {
let name = String::from("MapleCart");
let alias = name; // MOVED: the heap buffer has one owner
// println!("{name}"); // error: borrow of moved value
let len = measure(&alias); // borrow - the caller keeps ownership
println!("{alias} is {len} bytes long");
let mut buf = Vec::new();
fill(&mut buf); // exclusive borrow, and only here
}
fn measure(s: &str) -> usize { s.len() }
fn fill(v: &mut Vec<u8>) { v.push(1) }
Rule 3 is the one that earns its keep. Because a mutable borrow is exclusive, you cannot have the data race that C++ and Go both allow: no reader can observe a value while it is being written. Rust removes an entire class of bug by making the compiler reject the shape of the code, and it does it without a runtime check.
What it costs, honestly
Nothing at runtime: no reference counting, no tracing, no cycle collector. A
String is a pointer, a length and a capacity — 24 bytes — and dropping one calls
free. What it costs is design time. The borrow checker will refuse code
that a garbage collector would have accepted, and the fix is usually a real improvement:
copy a small value, narrow a borrow's scope, or restructure so that ownership lives in one
obvious place.
Two things make the experience much better than its reputation. The compiler's errors name
the conflicting borrow and its line, and if your function's signature is wrong the error
usually says so. And borrows end at their last use, not at the end of the
block, so a mutable borrow followed by an immutable one is fine as long as they do not
overlap. When a value genuinely must be shared, that is what Rc,
Arc and RefCell are for — but reaching for them first is how you
write Rust that fights you forever. Next: the error half of the language, which is as
unusual as the memory half.