Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
15: References and Borrowing
I've noticed a pattern with almost everyone I've mentored moving from C++ or Java to Rust. They usually start with this mindset: "References are just pointers that don't take ownership, so I can just pass them around and use them whenever I want."
In those other languages, that's mostly true. But in Rust, that way of thinking will lead you straight into a wall of compiler errors. Let me show you exactly why that's wrong with a piece of code that looks perfectly reasonable but will refuse to compile.
let mut players = vec!["Alice", "Bob", "Charlie"];
for player in &players {
if *player == "Alice" {
players.push("Guest"); // 💥 Error: cannot borrow `players` as mutable because it is also borrowed as immutable
}
}
If you're thinking, "Why? I'm just adding a name to the end of the list," you're thinking like a C++ programmer. To the Rust compiler, this is a disaster waiting to happen. While you are iterating (borrowing the list immutably), you're trying to modify that same list (borrowing it mutably). If adding "Guest" causes the vector to reallocate its memory to a new location, the reference the loop is currently holding would suddenly point to deleted memory. Rust stops this before it ever happens.
The "Pointers are Free" Myth vs. The Borrowing Contract
The core shift you need to make is this: a reference in Rust isn't just a memory address; it's a contract. When you create a reference, you aren't just pointing to data—you are locking that data into a specific state for as long as that reference exists.
There are two types of these contracts:
- Immutable Borrowing (&T): You're saying, "I need to read this, and I promise that as long as I'm looking at it, nobody will change it." You can have as many of these as you want simultaneously.
- Mutable Borrowing (&mut T): You're saying, "I need to change this, and I demand that I am the only person with access to it right now."
I like to think of it as a library book. Lots of people can read the same book at once (immutable), but if someone wants to edit the text in the book (mutable), they have to take it home, and nobody else can even look at it until they bring it back.
The "One Writer or Many Readers" Rule
This is the golden rule of Rust borrowing. You can have either one mutable reference or any number of immutable references. You can never have both at the same time.
Let's fix that player list example from earlier. If we want to modify the list while checking its contents, we have to make sure the immutable borrow ends before the mutable borrow begins. I usually do this by collecting the changes first, or by using indices instead of references.
let mut players = vec!["Alice", "Bob", "Charlie"];
let mut to_add = Vec::new();
for player in &players {
if *player == "Alice" {
to_add.push("Guest");
}
}
// The immutable borrow of `players` ended when the loop finished.
// Now we are free to borrow it mutably.
players.extend(to_add);
It feels like more work at first, but this is exactly how Rust eliminates "data races" at compile time. You'll stop worrying about whether a variable changed behind your back because the compiler literally won't let that happen.
Handling References in Functions
When you're designing your functions, you'll often wonder: "Should I take a reference or take ownership?" My rule of thumb is simple: start with a reference. If the function doesn't need to own the data to store it in a struct or send it to another thread, don't take ownership. It makes your API much more flexible for the caller.
struct Scoreboard {
scores: Vec<i32>,
}
impl Scoreboard {
// I use &self here because calculating the total shouldn't
// destroy the scoreboard or prevent others from reading it.
fn total_score(&self) -> i32 {
self.scores.iter().sum()
}
// I use &mut self because updating a score changes the state.
fn update_score(&mut self, index: usize, value: i32) {
self.scores[index] = value;
}
}
Remember, the "scope" of a borrow lasts from the moment the reference is created until the last time it's used. Rust is actually quite smart about this (it's called Non-Lexical Lifetimes), so you don't always have to wrap everything in curly braces to "end" a borrow, but you still have to respect the One Writer/Many Readers rule.
📋 Practical Task
Fixing the Inventory Management System
You are building a simple RPG inventory system. Below is a broken piece of code. The programmer tried to create a function that checks if an item exists in the inventory and, if it does, logs a message using a reference to that item. However, they also tried to update the item's quantity at the same time, which is triggering the borrow checker.
Your Goal: Modify the process_item function so that it compiles. You must ensure that the immutable borrow (used for logging) is finished before the mutable borrow (used for updating the quantity) begins.
struct Item {
name: String,
quantity: u32,
}
struct Inventory {
items: Vec<Item>,
}
impl Inventory {
fn process_item(&mut self, item_name: &str) {
// BUG: This loop creates an immutable borrow of self.items
for item in &self.items {
if item.name == item_name {
println!("Found item: {} with quantity {}", item.name, item.quantity);
// ERROR: Trying to borrow self mutably while the loop
// is still borrowing it immutably!
self.update_quantity(item_name, 1);
}
}
}
fn update_quantity(&mut self, name: &str, amount: u32) {
if let Some(item) = self.items.iter_mut().find(|i| i.name == name) {
item.quantity += amount;
}
}
}
fn main() {
let mut my_inv = Inventory {
items: vec![
Item { name: "Health Potion".to_string(), quantity: 5 },
Item { name: "Iron Sword".to_string(), quantity: 1 },
],
};
my_inv.process_item("Health Potion");
println!("New quantity: {}", my_inv.items[0].quantity);
}There are no comments for now.