Ownership: One Rule, Three Corollaries
Ownership is one rule with three corollaries. Each value has one owner. When the owner goes out of scope, the value is dropped. Assigning a value moves it, and the old name is gone.
This is weird at first. You will write code that looks obviously correct and the compiler will refuse it. That is the compiler doing its job. Read the message, adjust, move on.
For Python developers: this is the biggest mental shift. Python uses reference counting and a garbage collector. Rust uses ownership. Take your time.
The Problem Ownership Solves
Memory Management Approaches
| Approach | Languages | Pros | Cons |
|---|---|---|---|
| Manual | C, C++ | Fast, precise control | Memory leaks, dangling pointers |
| Garbage Collection | Java, Python, Go | Easy, safe | Runtime overhead, unpredictable pauses |
| Ownership | Rust | Safe, no runtime overhead | Learning curve, compiler strictness |
Python's approach:
# Python: GC manages memory automatically
s = "hello"
t = s # Both reference same object
# GC cleans up when no references remain
Rust's approach:
// Rust: Ownership system manages memory at compile time
let s = String::from("hello");
let t = s; // s moved to t, s no longer valid
// Memory freed when t goes out of scope (no GC!)
The Ownership Rules
Three rules the compiler enforces:
- Each value has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
Variable Scope
fn main() {
{ // s is not valid here, not declared yet
let s = "hello"; // s is valid from this point forward
println!("{}", s); // s can be used here
} // scope ends, s is no longer valid
// println!("{}", s); // ERROR: s not in scope
}
The Stack vs The Heap
Stack
- Fixed size data
- Fast allocation/deallocation
- Last-in, first-out (LIFO)
- Examples: integers, floats, booleans, tuples, arrays
fn main() {
let x = 5; // Stored on stack
let y = x; // Copy x to y (both on stack)
println!("{} {}", x, y); // Both valid
}
Heap
- Dynamic size data
- Slower allocation/deallocation
- Examples: String, Vec, HashMap
fn main() {
let s1 = String::from("hello"); // Stored on heap
let s2 = s1; // s1 moved to s2
// println!("{}", s1); // ERROR: s1 no longer valid
println!("{}", s2); // OK
}
Ownership and Move
Move Semantics
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // ERROR: value borrowed after move
println!("{}", s2); // OK
}
Python comparison:
# Python: Both variables reference same object
s1 = "hello"
s2 = s1 # s1 and s2 both valid, point to same string
print(s1) # OK
print(s2) # OK
What happens in Rust:
s1is created on the stack (pointer, length, capacity)- The actual data ("hello") is on the heap
s2 = s1copies the stack data (pointer, not the heap data)s1is invalidated to prevent double-free
This is called a move, not a shallow copy.
Copy vs Move
Copy Types (implement Copy trait):
- All scalar types (integers, floats, bool, char)
- Tuples containing only Copy types
- Arrays of Copy types
fn main() {
// Copy
let x = 5;
let y = x;
println!("{} {}", x, y); // Both valid
// Move
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // ERROR: moved
println!("{}", s2); // OK
}
Cloning
To actually duplicate heap data:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy
println!("s1 = {}, s2 = {}", s1, s2); // Both valid
}
Warning: clone() can be expensive for large data.
Ownership and Functions
Passing Values
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // ERROR: s was moved
let x = 5;
makes_copy(x);
println!("{}", x); // OK: x was copied
}
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
fn main() {
let s1 = gives_ownership();
println!("{}", s1);
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
// println!("{}", s2); // ERROR: s2 was moved
println!("{}", s3); // OK
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // Returned and moved to the caller
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // Returned and moved to the caller
}
References and Borrowing
Passing ownership is tedious when you want to use a value later. References fix that. A reference lets you point at a value without taking ownership.
Immutable References
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrow s1
println!("The length of '{}' is {}.", s1, len); // s1 still valid
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope but doesn't own the String, so nothing happens
Borrowing: Creating a reference (&) is called borrowing.
Mutable References
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Borrowing Rules
The two rules:
- At any moment, you have either one mutable reference, or any number of immutable references. Never both at once.
- References must always be valid. No dangling pointers.
Multiple Immutable References: OK
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, {}", r1, r2, r3); // OK
}
Mutable and Immutable: ERROR
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
let r3 = &mut s; // ERROR: cannot borrow as mutable
println!("{}, {}, {}", r1, r2, r3);
}
Why? Users of immutable references don't expect the value to change!
Multiple Mutable References: ERROR
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ERROR: cannot borrow as mutable more than once
println!("{}, {}", r1, r2);
}
Why? Prevents data races at compile time.
Non-Lexical Lifetimes (NLL)
The scope of a reference ends when it's last used:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // OK: r1 and r2 are out of scope
println!("{}", r3);
}
Dangling References
Rust prevents dangling references at compile time:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // ERROR
let s = String::from("hello");
&s // We return a reference to s, but s is dropped!
} // s goes out of scope and is dropped
Solution: Return the String directly (transfer ownership):
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out
}
The Slice Type
A slice is a reference to a contiguous sequence of elements.
String Slices
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
// Shorthand
let hello = &s[..5]; // Same as [0..5]
let world = &s[6..]; // From 6 to end
let entire = &s[..]; // Entire string
println!("{} {}", hello, world);
}
Type: String slice is &str
String Literals are Slices
let s: &str = "Hello, world!";
String literals are stored in the binary and are immutable.
Slice as Function Parameter
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string); // Works with String
let my_string_literal = "hello world";
let word = first_word(my_string_literal); // Works with &str
println!("{}", word);
}
Best practice: use &str as the parameter type. It accepts both String (via deref) and string literals.
Array Slices
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]
assert_eq!(slice, &[2, 3]);
}
Common Patterns
Pattern 1: Read Only Access
fn process_data(data: &Vec<i32>) {
for item in data {
println!("{}", item);
}
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
process_data(&numbers);
println!("Numbers: {:?}", numbers); // Still valid
}
Pattern 2: Mutable Access
fn double_values(data: &mut Vec<i32>) {
for item in data.iter_mut() {
*item *= 2;
}
}
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
double_values(&mut numbers);
println!("{:?}", numbers); // [2, 4, 6, 8, 10]
}
Pattern 3: Taking Ownership
fn consume_data(data: Vec<i32>) {
println!("Consumed: {:?}", data);
} // data is dropped here
fn main() {
let numbers = vec![1, 2, 3];
consume_data(numbers);
// numbers is no longer valid
}
Pattern 4: Returning Owned Data
fn create_data() -> Vec<i32> {
vec![1, 2, 3, 4, 5]
}
fn main() {
let numbers = create_data();
println!("{:?}", numbers);
}
Ownership Cheatsheet
Moves
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2
// s1 is invalid
takes_ownership(s2); // s2 moved into function
// s2 is invalid
Borrows (Immutable)
let s = String::from("hello");
let r1 = &s; // Borrow
let r2 = &s; // Another borrow (OK)
// s, r1, r2 all valid
read_only(&s); // Borrow for function call
// s still valid
Borrows (Mutable)
let mut s = String::from("hello");
let r = &mut s; // Mutable borrow
// Only r is valid, not s
r.push_str(" world");
// After r is done, s is valid again
Copies
let x = 5;
let y = x; // Copy (not move)
// x and y both valid
takes_copy(x); // Copy into function
// x still valid
Practice Exercises
Exercise 1: Understanding Moves
Predict which lines will compile:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // Will this compile?
println!("{}", s2); // Will this compile?
}
Exercise 2: Fix the Borrowing
Fix this code:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{}", r1);
}
Exercise 3: Implement a Function
Write a function that:
- Takes a string slice
- Returns the number of words
- Doesn't take ownership
fn count_words(s: &str) -> usize {
// Your code here
}
fn main() {
let text = String::from("hello world rust");
let count = count_words(&text);
println!("Words: {}", count);
println!("Original: {}", text); // Should still work
}
Exercise 4: Largest Element
Write a function that finds the largest element in a slice without taking ownership:
fn largest(list: &[i32]) -> i32 {
// Your code here
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("Largest: {}", result);
println!("Numbers: {:?}", numbers); // Should still work
}
Common Mistakes
1. Using After Move
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // ERROR
Fix: Use clone() or borrow instead.
2. Multiple Mutable Borrows
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ERROR
Fix: Use only one mutable reference at a time.
3. Mixing Mutable and Immutable Borrows
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ERROR
println!("{}", r1);
Fix: Finish using immutable references before creating mutable ones.
4. Returning References to Local Variables
fn dangle() -> &String {
let s = String::from("hello");
&s // ERROR: s is dropped
}
Fix: Return the owned value: return s;
Ownership in Practice
Why Ownership Matters
// No garbage collection needed!
fn process_large_file() {
let contents = read_file("large.txt");
// Do something with contents
} // contents automatically freed here - no GC pause!
// No memory leaks!
fn create_data() -> Vec<i32> {
let data = vec![1, 2, 3];
data
} // Ownership transferred, caller is responsible
// No data races!
fn safe_concurrent() {
let mut data = vec![1, 2, 3];
// Can't create multiple mutable references
// Compiler prevents data races at compile time!
}
Key Takeaways
- Ownership gives memory safety without garbage collection
- Move semantics prevent double-free and use-after-free
- Borrowing lets you use values without taking ownership
- References are immutable (
&T) or mutable (&mut T) - You get many immutable, or exactly one mutable, never both
- Slices give safe views into sequences
- The compiler enforces these rules at compile time with zero runtime cost
Next Steps
Continue to 04-control-flow.md to learn if, loops, and pattern matching.
Quick Reference
// Ownership transfer
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2
// Borrowing (immutable)
let r = &s2;
// Borrowing (mutable)
let mut s3 = String::from("hello");
let r = &mut s3;
// Clone (deep copy)
let s4 = s3.clone();
// Slices
let slice = &s4[0..5];
let arr_slice = &[1, 2, 3][1..];