Chapter 4: Ownership - THE MOST IMPORTANT CONCEPT
Understanding Rust’s Unique Memory Management
Learning Objectives
By the end of this chapter, you’ll be able to:
- Understand ownership rules and how they differ from C++/.NET memory management
- Work confidently with borrowing and references
- Navigate lifetime annotations and understand when they’re needed
- Transfer ownership safely between functions and data structures
- Debug common ownership errors with confidence
- Apply ownership principles to write memory-safe, performant code
Why Ownership Matters: The Problem It Solves
Memory Management Comparison
| Language | Memory Management | Common Issues | Performance | Safety |
|---|---|---|---|---|
| C++ | Manual (new/delete, RAII) | Memory leaks, double-free, dangling pointers | High | Runtime crashes |
| C#/.NET | Garbage Collector | GC pauses, memory pressure | Medium | Runtime exceptions |
| Rust | Compile-time ownership | Compiler errors (not runtime!) | High | Compile-time safety |
The Core Problem
// C++ - Dangerous code that compiles
std::string* dangerous() {
std::string local = "Hello";
return &local; // ❌ Returning reference to local variable!
}
// This compiles but crashes at runtime
// C# - Memory managed but can still have issues
class Manager {
private List<string> items;
public IEnumerable<string> GetItems() {
items = null; // Oops!
return items; // ❌ NullReferenceException at runtime
}
}
#![allow(unused)] fn main() { // Rust - Won't compile, saving you from runtime crashes fn safe_rust() -> &str { let local = String::from("Hello"); &local // ❌ Compile error: `local` does not live long enough } // Error caught at compile time! }
The Three Rules of Ownership
Rule 1: Each Value Has a Single Owner
#![allow(unused)] fn main() { let s1 = String::from("Hello"); // s1 owns the string let s2 = s1; // Ownership moves to s2 // println!("{}", s1); // ❌ Error: value borrowed after move // Compare to C++: // std::string s1 = "Hello"; // s1 owns the string // std::string s2 = s1; // s2 gets a COPY (expensive!) // std::cout << s1; // ✅ Still works, s1 unchanged }
Rule 2: There Can Only Be One Owner at a Time
fn take_ownership(s: String) { // s comes into scope println!("{}", s); } // s goes out of scope and `drop` is called, memory freed fn main() { let s = String::from("Hello"); take_ownership(s); // s's value moves into function // println!("{}", s); // ❌ Error: value borrowed after move }
Rule 3: When the Owner Goes Out of Scope, the Value is Dropped
#![allow(unused)] fn main() { { let s = String::from("Hello"); // s comes into scope // do stuff with s } // s goes out of scope, memory freed automatically }
Move Semantics: Ownership Transfer
Understanding Moves
#![allow(unused)] fn main() { // Primitive types implement Copy trait let x = 5; let y = x; // x is copied, both x and y are valid println!("x: {}, y: {}", x, y); // ✅ Works fine // Complex types move by default let s1 = String::from("Hello"); let s2 = s1; // s1 is moved to s2 // println!("{}", s1); // ❌ Error: value borrowed after move println!("{}", s2); // ✅ Only s2 is valid // Clone when you need a copy let s3 = String::from("World"); let s4 = s3.clone(); // Explicit copy println!("s3: {}, s4: {}", s3, s4); // ✅ Both valid }
Copy vs Move Types
#![allow(unused)] fn main() { // Types that implement Copy (stored on stack) let a = 5; // i32 let b = true; // bool let c = 'a'; // char let d = (1, 2); // Tuple of Copy types // Types that don't implement Copy (may use heap) let e = String::from("Hello"); // String let f = vec![1, 2, 3]; // Vec<i32> let g = Box::new(42); // Box<i32> // Copy types can be used after assignment let x = a; // a is copied println!("a: {}, x: {}", a, x); // ✅ Both work // Move types transfer ownership let y = e; // e is moved // println!("{}", e); // ❌ Error: moved }
References and Borrowing
Immutable References (Shared Borrowing)
fn calculate_length(s: &String) -> usize { // s is a reference s.len() } // s goes out of scope, but doesn't own data, so nothing happens fn main() { let s1 = String::from("Hello"); let len = calculate_length(&s1); // Pass reference println!("Length of '{}' is {}.", s1, len); // ✅ s1 still usable }
Mutable References (Exclusive Borrowing)
fn change(s: &mut String) { s.push_str(", world"); } fn main() { let mut s = String::from("Hello"); change(&mut s); // Pass mutable reference println!("{}", s); // Prints: Hello, world }
The Borrowing Rules
Rule 1: Either one mutable reference OR any number of immutable references
#![allow(unused)] fn main() { let mut s = String::from("Hello"); // ✅ Multiple immutable references let r1 = &s; let r2 = &s; println!("{} and {}", r1, r2); // OK // ❌ Cannot have mutable reference with immutable ones let r3 = &s; let r4 = &mut s; // Error: cannot borrow as mutable }
Rule 2: References must always be valid (no dangling references)
#![allow(unused)] fn main() { fn dangle() -> &String { // Returns reference to String let s = String::from("hello"); &s // ❌ Error: `s` does not live long enough } // s is dropped, reference would be invalid // ✅ Solution: Return owned value fn no_dangle() -> String { let s = String::from("hello"); s // Move s out, no reference needed } }
Reference Patterns in Practice
// Good: Take references when you don't need ownership fn print_length(s: &str) { // &str works with String and &str println!("Length: {}", s.len()); } // Good: Take mutable reference when you need to modify fn append_exclamation(s: &mut String) { s.push('!'); } // Sometimes you need ownership fn take_and_process(s: String) -> String { // Do expensive processing that consumes s format!("Processed: {}", s.to_uppercase()) } fn main() { let mut text = String::from("Hello"); print_length(&text); // Borrow immutably append_exclamation(&mut text); // Borrow mutably let result = take_and_process(text); // Transfer ownership // text is no longer valid here println!("{}", result); }
Lifetimes: Ensuring Reference Validity
Why Lifetimes Exist
#![allow(unused)] fn main() { // The compiler needs to ensure this is safe: fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } // Question: How long should the returned reference live? }
Lifetime Annotation Syntax
#![allow(unused)] fn main() { // Explicit lifetime annotations fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // The lifetime 'a means: // - x and y must both live at least as long as 'a // - The returned reference will live as long as 'a // - 'a is the shorter of the two input lifetimes }
Lifetime Elision Rules (When You Don’t Need Annotations)
Rule 1: Each reference parameter gets its own lifetime
#![allow(unused)] fn main() { // This: fn first_word(s: &str) -> &str { /* ... */ } // Is actually this: fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } }
Rule 2: If there’s exactly one input lifetime, it’s assigned to all outputs
#![allow(unused)] fn main() { // These are equivalent: fn get_first(list: &Vec<String>) -> &String { &list[0] } fn get_first<'a>(list: &'a Vec<String>) -> &'a String { &list[0] } }
Rule 3: Methods with &self give output the same lifetime as self
#![allow(unused)] fn main() { impl<'a> Person<'a> { fn get_name(&self) -> &str { // Implicitly &'a str self.name } } }
Complex Lifetime Examples
#![allow(unused)] fn main() { // Multiple lifetimes fn compare_and_return<'a, 'b>( x: &'a str, y: &'b str, return_first: bool ) -> &'a str { // Always returns something with lifetime 'a if return_first { x } else { y } // ❌ Error: y has wrong lifetime } // Fixed version - both inputs must have same lifetime fn compare_and_return<'a>( x: &'a str, y: &'a str, return_first: bool ) -> &'a str { if return_first { x } else { y } // ✅ OK } }
Structs with Lifetimes
// Struct holding references needs lifetime annotation struct ImportantExcerpt<'a> { part: &'a str, // This reference must live at least as long as the struct } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {}", announcement); self.part // Returns reference with same lifetime as &self } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; // i is valid as long as novel is valid }
Static Lifetime
#![allow(unused)] fn main() { // 'static means the reference lives for the entire program duration let s: &'static str = "I have a static lifetime."; // String literals // Static variables static GLOBAL_COUNT: i32 = 0; let count_ref: &'static i32 = &GLOBAL_COUNT; // Sometimes you need to store static references struct Config { name: &'static str, // Must be a string literal or static } }
Advanced Ownership Patterns
Returning References from Functions
#![allow(unused)] fn main() { // ❌ Cannot return reference to local variable fn create_and_return() -> &str { let s = String::from("hello"); &s // Error: does not live long enough } // ✅ Return owned value instead fn create_and_return_owned() -> String { String::from("hello") } // ✅ Return reference to input (with lifetime) fn get_first_word(text: &str) -> &str { text.split_whitespace().next().unwrap_or("") } }
Ownership with Collections
fn main() { let mut vec = Vec::new(); // Adding owned values vec.push(String::from("hello")); vec.push(String::from("world")); // ❌ Cannot move out of vector by index // let first = vec[0]; // Error: cannot move // ✅ Borrowing is fine let first_ref = &vec[0]; println!("First: {}", first_ref); // ✅ Clone if you need ownership let first_owned = vec[0].clone(); // ✅ Or use into_iter() to transfer ownership for item in vec { // vec is moved here println!("Owned item: {}", item); } // vec is no longer usable }
Splitting Borrows
#![allow(unused)] fn main() { // Sometimes you need to borrow different parts of a struct struct Point { x: f64, y: f64, } impl Point { // ❌ This won't work - can't return multiple mutable references // fn get_coords_mut(&mut self) -> (&mut f64, &mut f64) { // (&mut self.x, &mut self.y) // } // ✅ This works - different fields can be borrowed separately fn update_coords(&mut self, new_x: f64, new_y: f64) { self.x = new_x; // Borrow x mutably self.y = new_y; // Borrow y mutably (different field) } } }
Common Ownership Patterns and Solutions
Pattern 1: Function Parameters
#![allow(unused)] fn main() { // ❌ Don't take ownership unless you need it fn process_text(text: String) -> usize { text.len() // We don't need to own text for this } // ✅ Better: take a reference fn process_text(text: &str) -> usize { text.len() } // ✅ When you do need ownership: fn store_text(text: String) -> Box<String> { Box::new(text) // We're storing it, so ownership makes sense } }
Pattern 2: Return Values
#![allow(unused)] fn main() { // ✅ Return owned values when creating new data fn create_greeting(name: &str) -> String { format!("Hello, {}!", name) } // ✅ Return references when extracting from input fn get_file_extension(filename: &str) -> Option<&str> { filename.split('.').last() } }
Pattern 3: Structs Holding Data
// ✅ Own data when struct should control lifetime #[derive(Debug)] struct User { name: String, // Owned email: String, // Owned } // ✅ Borrow when data lives elsewhere #[derive(Debug)] struct UserRef<'a> { name: &'a str, // Borrowed email: &'a str, // Borrowed } // Usage fn main() { // Owned version - can outlive source data let user = User { name: String::from("Alice"), email: String::from("alice@example.com"), }; // Borrowed version - tied to source data lifetime let name = "Bob"; let email = "bob@example.com"; let user_ref = UserRef { name, email }; }
Debugging Ownership Errors
Common Error Messages and Solutions
1. “Value borrowed after move”
#![allow(unused)] fn main() { // ❌ Problem let s = String::from("hello"); let s2 = s; // s moved here println!("{}", s); // Error: value borrowed after move // ✅ Solutions // Option 1: Use references let s = String::from("hello"); let s2 = &s; // Borrow instead println!("{} {}", s, s2); // Option 2: Clone when you need copies let s = String::from("hello"); let s2 = s.clone(); // Explicit copy println!("{} {}", s, s2); }
2. “Cannot borrow as mutable”
#![allow(unused)] fn main() { // ❌ Problem let s = String::from("hello"); // Immutable s.push_str(" world"); // Error: cannot borrow as mutable // ✅ Solution: Make it mutable let mut s = String::from("hello"); s.push_str(" world"); }
3. “Borrowed value does not live long enough”
#![allow(unused)] fn main() { // ❌ Problem fn get_string() -> &str { let s = String::from("hello"); &s // Error: does not live long enough } // ✅ Solutions // Option 1: Return owned value fn get_string() -> String { String::from("hello") } // Option 2: Use string literal (static lifetime) fn get_string() -> &'static str { "hello" } }
Tools for Understanding Ownership
#![allow(unused)] fn main() { fn debug_ownership() { let s1 = String::from("hello"); println!("s1 created"); let s2 = s1; // Move occurs here println!("s1 moved to s2"); // println!("{}", s1); // This would error let s3 = &s2; // Borrow s2 println!("s2 borrowed as s3: {}", s3); drop(s2); // Explicit drop println!("s2 dropped"); // println!("{}", s3); // This would error - s2 was dropped } }
Performance Implications
Zero-Cost Abstractions
#![allow(unused)] fn main() { // All of these have the same runtime performance: // Direct access let vec = vec![1, 2, 3, 4, 5]; let sum1 = vec[0] + vec[1] + vec[2] + vec[3] + vec[4]; // Iterator (zero-cost abstraction) let sum2: i32 = vec.iter().sum(); // Reference passing (no copying) fn sum_vec(v: &Vec<i32>) -> i32 { v.iter().sum() } let sum3 = sum_vec(&vec); // All compile to similar assembly code! }
Memory Layout Guarantees
#![allow(unused)] fn main() { // Rust guarantees memory layout #[repr(C)] // Compatible with C struct layout struct Point { x: f64, // Guaranteed to be first y: f64, // Guaranteed to be second } // No hidden vtables, no GC headers // What you see is what you get in memory }
Key Takeaways
- Ownership prevents entire classes of bugs at compile time
- Move semantics are default - be explicit when you want copies
- Borrowing allows safe sharing without ownership transfer
- Lifetimes ensure references are always valid but often inferred
- The compiler is your friend - ownership errors are caught early
- Zero runtime cost - all ownership checks happen at compile time
Mental Model Summary
#![allow(unused)] fn main() { // Think of ownership like keys to a house: let house_keys = String::from("keys"); // You own the keys let friend = house_keys; // You give keys to friend // house_keys is no longer valid // You no longer have keys let borrowed_keys = &friend; // Friend lets you borrow keys // friend still owns keys // Friend still owns them drop(friend); // Friend moves away // borrowed_keys no longer valid // Your borrowed keys invalid }
Exercises
Exercise 1: Ownership Transfer Chain
Create a program that demonstrates ownership transfer through a chain of functions:
// Implement these functions following ownership rules fn create_message() -> String { // Create and return a String } fn add_greeting(message: String) -> String { // Take ownership, add "Hello, " prefix, return new String } fn add_punctuation(message: String) -> String { // Take ownership, add "!" suffix, return new String } fn print_and_consume(message: String) { // Take ownership, print message, let it be dropped } fn main() { // Chain the functions together // create -> add_greeting -> add_punctuation -> print_and_consume // Try to use the message after each step - what happens? }
Exercise 2: Reference vs Ownership
Fix the ownership issues in this code:
fn analyze_text(text: String) -> (usize, String) { let word_count = text.split_whitespace().count(); let uppercase = text.to_uppercase(); (word_count, uppercase) } fn main() { let article = String::from("Rust is a systems programming language"); let (count, upper) = analyze_text(article); println!("Original: {}", article); // ❌ This should work but doesn't println!("Word count: {}", count); println!("Uppercase: {}", upper); // Also make this work: let count2 = analyze_text(article).0; // ❌ This should also work }
Exercise 3: Lifetime Annotations
Implement a function that finds the longest common prefix of two strings:
// Fix the lifetime annotations fn longest_common_prefix(s1: &str, s2: &str) -> &str { let mut i = 0; let s1_chars: Vec<char> = s1.chars().collect(); let s2_chars: Vec<char> = s2.chars().collect(); while i < s1_chars.len() && i < s2_chars.len() && s1_chars[i] == s2_chars[i] { i += 1; } &s1[..i] // Return slice of first string } #[cfg(test)] mod tests { use super::*; #[test] fn test_common_prefix() { assert_eq!(longest_common_prefix("hello", "help"), "hel"); assert_eq!(longest_common_prefix("rust", "ruby"), "ru"); assert_eq!(longest_common_prefix("abc", "xyz"), ""); } } fn main() { let word1 = String::from("programming"); let word2 = "program"; let prefix = longest_common_prefix(&word1, word2); println!("Common prefix: '{}'", prefix); // Both word1 and word2 should still be usable here println!("Word1: {}, Word2: {}", word1, word2); }
Next Up: In Chapter 5, we’ll explore smart pointers - Rust’s tools for more complex memory management scenarios when simple ownership isn’t enough.