Rust by Example: Ownership Basics
Master the most unique feature of Rust: Ownership. This example illustrates the three rules of ownership, the difference between stack and heap allocation, and how 'move' semantics prevent memory safety bugs without a garbage collector.
Code
fn main() {
// --- Stack Data (Copy) ---
let x = 5;
let y = x; // x is COPIED to y
println!("x: {}, y: {}", x, y); // Both are valid
// --- Heap Data (Move) ---
let s1 = String::from("hello");
// s1 is MOVED to s2
// s1 is no longer valid after this line
let s2 = s1;
// println!("{}, world!", s1); // This would cause a compile error!
println!("{}, world!", s2); // s2 is valid
// --- Cloning ---
let s3 = String::from("deep copy");
let s4 = s3.clone(); // Deep copy of heap data
println!("s3: {}, s4: {}", s3, s4); // Both are valid
}Explanation
Ownership is Rust's most unique feature and is what enables memory safety without a garbage collector. The rules are simple but strict: 1) Each value in Rust has a variable that's called its owner. 2) There can only be one owner at a time. 3) When the owner goes out of scope, the value will be dropped (freed).
For simple types like integers (which live on the stack), assigning one variable to another makes a copy of the data. However, for complex types like String (which store data on the heap), assigning let s2 = s1; does not copy the data. Instead, it performs a Move operation.
- The Move: Rust copies the "fat pointer" (pointer, length, capacity) from
s1tos2. This is a cheap stack-only copy. - Invalidation: To prevent "double free" errors (where both variables try to free the same heap memory), Rust considers
s1invalid after the move. - RAII: When a variable goes out of scope, Rust automatically calls its
dropfunction to clean up resources. This is the Resource Acquisition Is Initialization pattern.
If you genuinely want to duplicate the heap data, you must use the .clone() method. This performs a "deep copy," creating a new allocation on the heap. This is more expensive than a move, so Rust forces you to be explicit about it.
Code Breakdown
let y = x;. Since x is an integer (a primitive type with a known size at compile time), it implements the Copy trait. The value 5 is copied onto the stack for y, so x remains valid.let s2 = s1;. String does not implement Copy. This is a "Move" operation. The pointer, length, and capacity are copied to s2, but s1 is invalidated. This prevents memory corruption.s3.clone(). This explicitly requests a deep copy of the heap data. Both s3 and s4 now own their own independent copies of the string "deep copy", so both are valid.
