Rust Systems Programming Mastery
Ownership and Borrowing
The Rules of Ownership
Most programming languages manage memory in one of two ways: with a garbage collector that cleans up unused memory at runtime, or by making the programmer manually allocate and free memory. Rust takes a third path. It uses a system of ownership with rules the compiler enforces at compile time. This system manages memory safely and efficiently without runtime overhead.
Ownership is a set of rules that govern how a Rust program manages memory.
This whole system is built on three simple rules. If you understand them, you understand ownership.
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
Move vs. Copy
Let's look at how these rules affect program behavior. Simple data types with a known, fixed size are stored entirely on the stack. When you assign them to another variable, Rust makes a full copy. Both variables are independent.
// x is created on the stack
let x = 5;
// A copy of the value in x is made for y
let y = x;
// Both x and y are valid and hold the value 5
println!("x = {}, y = {}", x, y);
But what about complex data stored on the heap? Consider a String, which can grow and shrink. It stores its text data on the heap and a pointer to that data on the stack. If you assign one String to another, Rust doesn't copy the heap data. That would be slow.
Instead, it moves ownership. The pointer, length, and capacity on the stack are copied, but the pointer still points to the same heap data. To prevent memory errors like a double free, Rust invalidates the original variable. It's no longer the owner.
let s1 = String::from("hello");
// Ownership of the heap data is moved from s1 to s2
let s2 = s1;
// This line would cause a compile error!
// s1 is no longer a valid owner.
// println!("{}", s1);
This is called a move. The ownership was moved from s1 to s2. If you need a deep copy of heap data, you can use a common method called clone().
Borrowing and References
Moving ownership every time you want to pass data to a function would be tedious. Imagine a function that calculates the length of a string. If it took ownership, the original string variable would become invalid after the call.
This is where references come in. A reference allows you to refer to a value without taking ownership of it. This is called borrowing. You can create a reference by using the ampersand &.
fn main() {
let s1 = String::from("hello");
// Pass a reference to s1
let len = calculate_length(&s1);
// s1 is still valid here!
println!("The length of '{}' is {}.", s1, len);
}
// This function borrows a String
fn calculate_length(s: &String) -> usize {
s.len()
}
Just like variables, references are immutable by default. You can't modify something through an immutable reference. If you need to change the borrowed data, you can create a mutable reference using &mut.
However, Rust enforces a critical rule to prevent data races: you can have either one mutable reference or any number of immutable references to a particular piece of data in a particular scope. You cannot have both.
A data race occurs when two or more pointers access the same data at the same time, at least one of them is writing, and there's no mechanism to synchronize their access. This leads to unpredictable behavior. The compiler's enforcement of this borrowing rule prevents data races from ever happening.
One mutable reference OR many immutable references. Never both at the same time.
This rule is enforced by the —the part of the Rust compiler that ensures all borrows are valid. It's a key reason why Rust can guarantee memory safety and prevent entire classes of bugs at compile time. It might seem strict at first, but it guides you toward writing safer, more explicit code.
Let's check your understanding of these core concepts.
How does Rust's approach to memory management differ from languages that use a garbage collector or require manual memory management?
What happens in Rust when you assign a variable of a complex data type like String to another variable?
Ownership is Rust's most unique feature. By managing memory with these compile-time rules, Rust provides the performance of a low-level language with strong safety guarantees, all without needing a garbage collector.