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
19: The Borrow Checker in Practice
By now, you've probably run into the borrow checker. You've likely felt that specific frustration where you know the code is logically sound, but the compiler is screaming at you that you're violating some rule about mutable and immutable borrows. It feels like the compiler is fighting you, but in reality, it's preventing a memory disaster that would have taken you three days to debug in C++.
Let's look at a scenario I see all the time. Imagine we're building a simple user management system. We want to find a specific user in a list and, if they meet a certain condition, add a "log" entry to the end of that same list.
struct User {
id: u32,
name: String,
is_active: bool,
}
fn process_users(users: &mut Vec<User>) {
for user in users.iter() {
if !user.is_active {
println!("User {} is inactive. Adding a system alert.", user.name);
// We want to add a dummy "System" user as an alert
users.push(User {
id: 0,
name: "SYSTEM_ALERT".to_string(),
is_active: true,
});
}
}
}
If you try to compile this, Rust will hit you with a wall of text. The core of the error is: cannot borrow `*users` as mutable because it is also borrowed as immutable. To a beginner, this feels contradictory. You passed users as &mut Vec, so why is it complaining about an immutable borrow?
The Conflict Between Iterators and Mutation
Here is what's happening under the hood. When you call users.iter(), you are creating an immutable borrow of the entire vector. The user variable in your loop is a reference pointing directly into the memory owned by that vector. As long as that loop is running, the borrow checker considers the vector "locked" for reading.
Then, inside the loop, you call users.push(...). Pushing to a vector requires a mutable borrow. But here's the danger: if the vector is full and needs to grow, Rust has to allocate a larger chunk of memory elsewhere and move all the existing elements there. If Rust allowed this, the user reference you're currently holding would suddenly point to old, deallocated memory. That's a use-after-free bug, and Rust's primary job is to make those impossible.
Decoupling the Search from the Modification
To fix this, we have to stop holding a reference to an element while we are trying to change the structure of the collection. I usually handle this in one of two ways, depending on the goal. If you just need to know which element to change or act upon, don't store the reference—store the index.
By using .position() or a simple range loop, we can find the "where" and then perform the "what" separately.
fn process_users(users: &mut Vec<User>) {
let mut alert_indices = Vec::new();
// Phase 1: Immutable borrow.
// We find the indices of inactive users and store them in a separate list.
for (index, user) in users.iter().enumerate() {
if !user.is_active {
alert_indices.push(index);
}
}
// Phase 2: Mutable borrow.
// The immutable borrow from the loop above has ended.
for idx in alert_indices {
println!("User {} is inactive. Adding a system alert.", users[idx].name);
users.push(User {
id: 0,
name: "SYSTEM_ALERT".to_string(),
is_active: true,
});
}
}
Notice the shift. In the first loop, we borrow users immutably. Once that loop finishes, that borrow ends. Now the vector is "unlocked," and we are free to borrow it mutably to push new elements. I've effectively split the logic into a "Read Phase" and a "Write Phase."
Another quick tip: if you only need to mutate the elements themselves and not change the size of the vector (no pushing or popping), use iter_mut(). But the moment you need to change the capacity or length of the collection, you must ensure no references to the interior of that collection are still alive.
📋 Practical Task
Exercise: Fixing the Message Buffer Overflow
You are working on a chat application. You have a MessageBuffer that stores a list of strings. You need to implement a function called cleanup_buffer that iterates through the messages and, if it finds a message that is "too long" (over 100 characters), it should remove that message and add a "Message truncated" warning to the end of the buffer.
The following code is broken and will not compile. Your task is to refactor it so that it satisfies the borrow checker while maintaining the logic: find the long messages, remove them, and push a warning for each one removed.
struct MessageBuffer {
messages: Vec<String>,
}
impl MessageBuffer {
fn cleanup_buffer(&mut self) {
// BUG: This loop borrows self.messages immutably
for msg in self.messages.iter() {
if msg.len() > 100 {
// BUG: This attempts to borrow self.messages mutably
// while the iterator above is still active.
self.messages.push("Warning: Message truncated".to_string());
// Note: You'll also need to figure out how to remove
// the original long message without breaking the loop!
}
}
}
}
fn main() {
let mut buffer = MessageBuffer {
messages: vec![
"Hello!".to_string(),
"This is a very long message...".repeat(10), // Over 100 chars
"Short msg".to_string(),
],
};
buffer.cleanup_buffer();
println!("Buffer size: {}", buffer.messages.len());
}
Requirements:
- The final code must compile.
- For every message over 100 characters, the original must be removed and a "Warning: Message truncated" string must be added to the end.
- Avoid using
unsafeblocks.
There are no comments for now.