#5 Ownership
move, copy, drop
What is ownership?
Ownership is Rust's most distinctive feature. It is a set of rules that the compiler checks at compile time to manage memory. No garbage collector is needed, and no manual allocation and deallocation is required.
Ownership is what makes Rust unique: it guarantees memory safety without any runtime cost.
The three rules
The ownership system is built on three simple rules:
// 1. Each value in Rust has one (and only one) owner.
// 2. When the owner goes out of scope, the value is dropped.
// 3. There can only be one owner at a time.
{
let s = String::from("hello"); // s is the owner
// s is valid here
} // s goes out of scope — memory is freed automaticallyWhen a variable goes out of scope (exits the {} block), Rust automatically calls drop to free its memory. This is deterministic — you always know when memory is freed.
Move semantics
When you assign a heap-allocated value to another variable, the ownership moves. The original variable becomes invalid:
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
println!("{s2}"); // OK
// println!("{s1}"); // ERROR: s1 was movedThis prevents double free errors. Only one variable owns the data, so only one will free it when it goes out of scope.
If you need a deep copy, use clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy
println!("s1 = {s1}"); // OK — both are valid
println!("s2 = {s2}"); // OKThe Copy trait
Simple types stored entirely on the stack implement the Copy trait. For these types, assignment creates a copy instead of a move:
let x = 5;
let y = x; // Copy, not move!
println!("x = {x}, y = {y}"); // Both are valid!
// Types that implement Copy:
// - Integers (i32, u64, etc.)
// - Floating-point (f32, f64)
// - Booleans (bool)
// - Characters (char)
// - Tuples of Copy types: (i32, f64)Ownership and functions
Passing a value to a function follows the same rules. Heap values are moved, stack values are copied:
fn greet(name: String) {
println!("Hello, {name}!");
} // name is dropped here
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // Return ownership back
}
fn main() {
let name = String::from("Alice");
greet(name);
// println!("{name}"); // ERROR: name was moved into greet
let s = String::from("hello");
let (s, len) = calculate_length(s);
println!("Length of \"{s}\" is {len}");
}Returning values transfers ownership back to the caller. But passing ownership back and forth is cumbersome — that's why Rust has references, which we'll learn about in the next lesson.
Your turn
Try running the ownership demo with cargo run and cargo check in the terminal below: