Ownership: Rust's Superpower
Ownership is Rust's most unique feature. It enables memory safety without a garbage collector. Understanding ownership is crucial to mastering Rust.
For Python developers: This is the biggest mental shift. Python uses reference counting and garbage collection - Rust uses ownership. Take your time with this chapter!
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 simple rules that Rust enforces at compile time:
- 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
Problem: Passing ownership is tedious when you want to use a value later.
Solution: References allow you to refer to 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 Golden Rules:
- At any time, you can have either:
- One mutable reference, OR
- Any number of immutable references
- References must always be valid (no dangling references)
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 parameter type to accept both String 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 ensures memory safety without garbage collection
- Move semantics prevent double-free and use-after-free
- Borrowing allows using values without taking ownership
- References can be immutable (
&T) or mutable (&mut T) - You can have multiple immutable OR one mutable reference
- Slices provide safe views into sequences
- The compiler enforces these rules at compile time - zero runtime cost
Next Steps
Now that you understand ownership, the next chapter covers control flow: if statements, loops, and pattern matching - the tools you'll use to direct your programs.
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..];