Ownership in Rust
Ownership is Rust's most unique feature. It enables Rust to make memory safety guarantees without needing a garbage collector. Understanding ownership is essential to writing Rust programs. In this lesson, you'll learn the three ownership rules, how memory works, and why ownership makes Rust special.
What is Ownership?
Ownership is a set of rules that govern how Rust manages memory. All programs must manage the way they use a computer's memory while running. Some languages have garbage collection, others require manual memory management. Rust uses a third approach: memory is managed through a system of ownership with rules that the compiler checks at compile time.
The Three Rules of Ownership
Keep these rules in mind as we work through examples:
- 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 will be dropped.
Click Run to execute your code
The Stack and the Heap
To understand ownership, we need to understand how memory works. Both the stack and the heap are parts of memory available to your code at runtime, but they're structured differently.
The Stack
The stack stores values in the order it gets them and removes them in the opposite order (LIFO - Last In, First Out). All data stored on the stack must have a known, fixed size at compile time.
- Fast: Adding and removing data is very quick
- Fixed size: Size must be known at compile time
- Automatic: Memory is automatically managed
- Examples: Integers, booleans, chars, fixed-size arrays
The Heap
The heap is less organized. When you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot big enough, marks it as in use, and returns a pointer.
- Slower: Allocating requires finding space
- Dynamic size: Can grow or shrink at runtime
- Manual management: Must be explicitly freed (Rust does this via ownership)
- Examples: String, Vec, Box, HashMap
Click Run to execute your code
Move Semantics
When you assign a heap-allocated value to another variable, Rust moves the ownership rather than copying the data. This is different from many other languages!
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error! s1 is no longer valid
println!("{}", s2); // OK! s2 is the owner
s1 and
s2 to be valid, when they
both go out of scope, they would both try to free the same memory. This is
called a
double free error
and is a memory safety bug. Rust prevents this by invalidating s1.
The Copy Trait
Some types implement the Copy trait. When a type implements
Copy, variables are
copied instead of moved. This applies to simple scalar values stored entirely on
the stack.
| Copy Types | Move Types |
|---|---|
All integers (i32, u64, etc.) |
String |
Floating point (f32, f64) |
Vec<T> |
bool |
Box<T> |
char |
HashMap<K, V> |
| Tuples (if all elements are Copy) | Any type with heap data |
// Copy types - both variables are valid
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y); // Both work!
// Move types - only one owner
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // Error! s1 was moved
Deep Copying with Clone
If you do want to deeply copy heap data, you can use the
clone method:
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy of heap data
println!("s1: {}, s2: {}", s1, s2); // Both are valid!
clone can be expensive
because it copies all the heap
data. Use it only when you actually need a deep copy.
Ownership and Functions
Passing a value to a function will move or copy, just like assignment:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is moved into the function
// s is no longer valid here
let x = 5;
makes_copy(x); // x is copied
// x is still valid here
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string goes out of scope and is dropped
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer goes out of scope, nothing special happens
Returning Values and Ownership
Returning values can also transfer ownership:
Click Run to execute your code
Common Ownership Mistakes
1. Using a value after it's been moved
Wrong:
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // Error: value borrowed after move
Fix Option 1 - Clone:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1); // OK! s1 still owns its data
Fix Option 2 - Use references (next lesson):
let s1 = String::from("hello");
let s2 = &s1; // Borrow instead of move
println!("{}", s1); // OK! s1 still owns its data
2. Forgetting that functions take ownership
Wrong:
let s = String::from("hello");
print_string(s);
println!("{}", s); // Error: s was moved into function
fn print_string(text: String) {
println!("{}", text);
}
Fix - Return ownership:
let s = String::from("hello");
let s = print_and_return(s);
println!("{}", s); // OK!
fn print_and_return(text: String) -> String {
println!("{}", text);
text // Return ownership
}
3. Moving in a loop
Wrong:
let words = vec![String::from("hello")];
for word in words {
println!("{}", word);
}
println!("{:?}", words); // Error: words was moved
Fix - Iterate by reference:
let words = vec![String::from("hello")];
for word in &words { // Borrow each element
println!("{}", word);
}
println!("{:?}", words); // OK!
Ownership Best Practices
- Prefer borrowing over moving: We'll learn about references in the next lesson
- Use clone sparingly: It's expensive; only use when you truly need a deep copy
- Return ownership when needed: Functions can return values to give ownership back
- Understand Copy vs Move: Know which types implement Copy
- Let the compiler guide you: Ownership errors are caught at compile time with helpful messages
- Think about ownership early: Design your APIs with ownership in mind
Exercise: Ownership Practice
Task: Fix the ownership errors in the code.
Requirements:
- Fix moved value errors
- Use clone where appropriate
- Return ownership from functions
- Handle ownership in loops
Click Run to execute your code
Show Solution
fn main() {
// Fix 1: Clone s1
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1: {}, s2: {}", s1, s2);
// Fix 2: Return ownership
let s3 = String::from("world");
print_string_fixed(s3.clone());
println!("s3: {}", s3);
// Fix 3: Return tuple
let s4 = String::from("Rust");
let (s4, len) = calculate_length_fixed(s4);
println!("Length of '{}' is {}", s4, len);
// Fix 4: Return both values
let s5 = String::from("programming");
let (s5, s6) = append_exclamation_fixed(s5);
println!("s5: {}, s6: {}", s5, s6);
// Fix 5: Clone for copy
let x = String::from("copy");
let y = x.clone();
println!("x: {}, y: {}", x, y);
// Fix 6: Use iter()
let words = vec![
String::from("hello"),
String::from("world"),
];
for word in &words {
println!("{}", word);
}
println!("Words: {:?}", words);
}
fn print_string_fixed(s: String) {
println!("{}", s);
}
fn calculate_length_fixed(s: String) -> (String, usize) {
let len = s.len();
(s, len)
}
fn append_exclamation_fixed(s: String) -> (String, String) {
let mut result = s.clone();
result.push('!');
(s, result)
}
Summary
- Ownership is Rust's most unique feature for memory safety
- Three rules: Each value has one owner, only one owner at a time, value dropped when owner goes out of scope
- Stack: Fast, fixed-size, automatic management
- Heap: Slower, dynamic-size, ownership rules apply
- Move semantics: Heap values are moved, not copied by default
- Copy trait: Simple stack values are copied automatically
- Clone: Explicit deep copy for heap data
- Functions can take and return ownership
- Ownership errors are caught at compile time
- No garbage collector needed - zero runtime overhead!
What's Next?
Ownership is powerful, but taking and returning ownership with every function would be tedious. In the next lesson, you'll learn about references and borrowing - a way to use values without taking ownership. This makes Rust both safe and ergonomic.
Enjoying these tutorials?