Chapter 4 — Understanding Ownership
Rust's most unique feature. Memory safety without garbage collection.
The Three Rules of Ownership
- 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
Stack vs Heap
Stack: Fast, fixed-size data (integers, booleans, etc.)
Heap: Slower, dynamic-size data (String, Vec, etc.)
#![allow(unused)] fn main() { let x = 5; // stored on stack let s = String::from("hello"); // stored on heap }
Move Semantics
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is MOVED to s2 // println!("{s1}"); // ERROR! s1 no longer valid println!("{s2}"); // OK }
Why? Prevents double-free errors. Only s2 owns the data now.
Stack-only data copies:
#![allow(unused)] fn main() { let x = 5; let y = x; // x is COPIED (not moved) println!("{x}, {y}"); // Both valid! }
Clone (Deep Copy)
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); // expensive deep copy println!("{s1}, {s2}"); // Both valid }
Copy Trait
Types with Copy trait are copied instead of moved:
- All integers:
i32,u64, etc. - Booleans:
bool - Floats:
f64,f32 - Characters:
char - Tuples (if all elements are
Copy)
#![allow(unused)] fn main() { let x = 5; let y = x; // Copy happens automatically }
Ownership and Functions
fn main() { let s = String::from("hello"); takes_ownership(s); // s moved into function // println!("{s}"); // ERROR! s no longer valid let x = 5; makes_copy(x); // x copied into function println!("{x}"); // OK! x still valid } fn takes_ownership(some_string: String) { println!("{some_string}"); } // some_string dropped here fn makes_copy(some_integer: i32) { println!("{some_integer}"); }
Return values transfer ownership:
#![allow(unused)] fn main() { fn gives_ownership() -> String { String::from("yours") // returned value moves out } fn takes_and_gives_back(a_string: String) -> String { a_string // returned back to caller } }
References and Borrowing
References let you use a value without taking ownership.
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // borrow s1 println!("'{s1}' has length {len}"); // s1 still valid! } fn calculate_length(s: &String) -> usize { s.len() } // s goes out of scope, but doesn't drop the data (not owner)
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"); }
Reference Rules
- You can have either:
- One mutable reference, OR
- Any number of immutable references
- References must always be valid (no dangling references)
This fails:
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // ERROR! Can't have two mutable refs }
This works:
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; // OK! Multiple immutable refs allowed println!("{r1}, {r2}"); }
Scope matters:
#![allow(unused)] fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{r1}, {r2}"); // r1 and r2 no longer used after this let r3 = &mut s; // OK! No overlap with r1, r2 println!("{r3}"); }
Slices
Slices reference a contiguous sequence without taking ownership.
String slices:
#![allow(unused)] 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]; // from start let world = &s[6..]; // to end let whole = &s[..]; // entire string }
String literals are slices:
#![allow(unused)] fn main() { let s = "Hello, world!"; // type: &str (immutable) }
Array slices:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // [2, 3] }
Practical example:
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_literal = "hello world"; let word = first_word(my_literal); // works with &str }
Common Ownership Errors
Use after move:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; println!("{s1}"); // ERROR: value borrowed after move }
Dangling reference:
#![allow(unused)] fn main() { fn dangle() -> &String { // ERROR: returns reference to dropped value let s = String::from("hello"); &s } // s dropped here, but we're returning a reference to it! }
Fix: Return the String itself (transfer ownership):
#![allow(unused)] fn main() { fn no_dangle() -> String { let s = String::from("hello"); s // ownership moved out } }
Key Takeaways
- Ownership prevents memory bugs at compile time
- Move semantics by default for heap data
- Use references (
&) to borrow without taking ownership - Mutable refs are exclusive, immutable refs can be shared
- Slices are references to part of a collection
- Compiler enforces all these rules - no runtime overhead!