-
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
90: Whiteboard Practice: Solving Problems Under Borrow Checker Constraints
When you're in a technical interview or whiteboarding a new feature, it's easy to map out the logic in your head using a language like Python or Java. You think, "I'll just loop through this list and update the objects based on some other object in the same list." Then you start typing in Rust, and the borrow checker immediately shuts you down. It feels like the language is fighting your logic, but usually, it's just pointing out a memory safety risk you hadn't considered.
Let's walk through a scenario I ran into recently. I wanted to implement a simple "Mentor Assignment" system. We have a list of users, some of whom are mentors. I want to iterate through the students and update their mentor_id based on some matching criteria found elsewhere in the same list.
The Collision Course
Here is my first attempt. I'll define a simple User struct and a list of people. My goal is to find a mentor whose ID matches the student's preferred mentor ID and verify that the mentor actually exists before finalizing the assignment.
struct User {
id: u32,
name: String,
mentor_id: Option,
}
fn assign_mentors(users: &mut Vec<User>) {
for user in users {
if let Some(preferred_id) = user.mentor_id {
// I need to check if the mentor exists in the same list
if users.iter().any(|u| u.id == preferred_id) {
println!("Assigning {} to mentor {}", user.name, preferred_id);
}
}
}
}
If you try to compile this, the compiler will scream at you. It'll say something like: cannot borrow `*users` as immutable because it is also borrowed as mutable.
I can see why. The for user in users loop creates a mutable borrow of the entire Vec to give me that user reference. While I'm holding that mutable reference, I'm trying to call users.iter(), which requires an immutable borrow. Rust won't let me have both at the same time because if iter() somehow triggered a reallocation or a change in the vector, my user reference would become a dangling pointer. It's a classic "aliasing" problem.
Trying to Cheat with Scopes
My first instinct is usually to try and "drop" the borrow as quickly as possible. I might think, "What if I just find the mentor's existence first, store it in a boolean, and then update the user?"
for user in users {
let exists = if let Some(pid) = user.mentor_id {
users.iter().any(|u| u.id == pid) // Still fails!
} else {
false
};
// ... update user ...
}
Still fails. Why? Because the user reference is still alive during the users.iter() call. Even if I don't use user inside the any() closure, the loop has already claimed a mutable borrow of the vector for the duration of that iteration. The borrow checker isn't looking at what I'm actually doing with the memory; it's looking at the potential for a conflict.
Breaking the Link with Indices
When you hit this wall on a whiteboard, you have to stop thinking in terms of "references" and start thinking in terms of "ownership" or "indices." If I can't hold a reference to an element and the collection at the same time, I'll just stop holding the reference.
Instead of iterating via &mut User, I'll iterate using indices. This allows me to finish borrowing the vector for the search, and then borrow it again for the update.
fn assign_mentors(users: &mut Vec<User>) {
for i in 0..users.len() {
// 1. Borrow the vector immutably to check for the mentor
let preferred_id = users[i].mentor_id;
let mentor_exists = if let Some(pid) = preferred_id {
users.iter().any(|u| u.id == pid)
} else {
false
};
// 2. Now that the immutable borrow from .iter() is gone,
// we can borrow the vector mutably again to update the user
if mentor_exists {
let user = &mut users[i];
user.name = format!("{} (Assigned)", user.name);
}
}
}
This works perfectly. By using 0..users.len(), I'm just iterating over integers. The borrow of users[i] to get the preferred_id is short-lived, and the borrow for users.iter() is also short-lived. They don't overlap in a way that violates the rules.
The Trade-off
Now, you might be thinking, "Wait, isn't indexing slower or more dangerous because of bounds checking?" In a high-performance hot loop, maybe. But on a whiteboard or in general application logic, the clarity and correctness of the borrow checker are more important. You've traded a direct reference for a couple of index lookups, but you've solved the aliasing conflict.
Another option would have been to collect the IDs into a separate HashSet first, which would move the "search" data out of the Vec entirely. That's often the "cleaner" architectural choice, but when you're constrained to a specific data structure during a problem-solving session, indexing is your best friend.
📋 Practical Task
Exercise: The Mutual Friendship Linker
You are building a simplified social graph. You have a Vec<Person> where each Person has an id and a friends list (containing IDs of other people).
Your task is to implement a function ensure_mutual_friendships(people: &mut Vec<Person>). The function should iterate through every person; for every friend ID they have, it should check if that friend also has them in their friend list. If the friendship is not mutual, you must add the current person's ID to the friend's list.
Constraints:
- You must perform this operation in-place on the
Vec<Person>. - You will likely encounter the "cannot borrow as mutable because it is also borrowed as immutable" error. Use the indexing strategy discussed in the lesson to resolve this.
struct Person {
id: u32,
friends: Vec<u32>,
}
fn ensure_mutual_friendships(people: &mut Vec<Person>) {
// Your implementation here
}There are no comments for now.