-
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
144: Whiteboard Practice: Implementing a Linked List with Ownership Constraints
Implementing a linked list is a rite of passage in any language, but in Rust, it's less of a data structure exercise and more of a wrestling match with the borrow checker. When you're at a whiteboard, it's easy to draw arrows and think "this just points to that." In Rust, those arrows are ownership transfers, and if you don't account for them, the compiler will stop you dead in your tracks.
Here is a piece of code that looks perfectly logical to someone coming from Java or Python, but it's a disaster in Rust. I've written a basic LinkedList and a pop method that is supposed to remove the head of the list and return its value.
struct Node {
value: i32,
next: Option<Box<Node>>,
}
pub struct LinkedList {
head: Option<Box<Node>>,
}
impl LinkedList {
pub fn pop(&mut self) -> Option<i32> {
// The logic: take the head, move the pointer to the next node,
// and return the value of the old head.
if let Some(node) = self.head {
self.head = node.next; // ERROR: cannot move out of borrowed content
Some(node.value)
} else {
None
}
}
}
The "Move Out of Borrowed Content" Wall
If you try to compile this, you'll get a frustrating error: cannot move out of borrowed content. Why? Because self is a mutable reference (&mut self). When you do if let Some(node) = self.head, you aren't actually taking ownership of the node; you're just looking at it. However, the line self.head = node.next attempts to move the next pointer out of that borrowed node and assign it back into self.head.
Rust won't let you do this because, for a brief moment, you're trying to move a value out of a place that you don't own. If the compiler allowed this, you'd end up with a "hole" in your memory where the head used to be, which is exactly what Rust is designed to prevent.
Using .take() to Swap Ownership
To fix this, we need a way to tell Rust: "I want to take the value out of this Option and leave None in its place temporarily." This is exactly what the Option::take() method is for. It's the secret weapon for implementing linked lists and other recursive structures in Rust.
By using take(), we move the Box out of the head and leave None there. Now that we own the node, we can freely move the next pointer back into the head without fighting the borrow checker.
impl LinkedList { pub fn pop(&mut self) -> Option<i32> { // .take() replaces self.head with None and returns the original value self.head.take().map(|node| { // Now we own 'node', so we can move 'node.next' back into 'self.head' self.head = node.next; node.value }) } }I used
.map()here instead of anif letjust to keep it concise, but the magic is inself.head.take(). We've effectively swapped the ownership of the node from the list to the local variable, allowing us to dismantle the node and re-attach the rest of the list to the head.Keep in mind that this pattern—replacing a value with a default (like
None) to satisfy the borrow checker—is something you'll do constantly when dealing withBoxandOption. It's the idiomatic way to handle ownership transfers in mutable structures.
📋 Practical Task
Implementing an In-Place Linked List Reverser
Now it's your turn. You have a LinkedList implementation, but it's missing a crucial feature: the ability to reverse the list without allocating a new one. You must modify the list in-place by swapping the next pointers of the nodes.
Your Task: Implement the reverse method for the following LinkedList. You will need to use Option::take() to move the nodes as you iterate through the list, otherwise, you'll run into the same ownership issues we discussed above.
struct Node {
value: i32,
next: Option<Box<Node>>,
}
pub struct LinkedList {
head: Option<Box<Node>>,
}
impl LinkedList {
pub fn new() -> Self {
LinkedList { head: None }
}
pub fn push(&mut self, value: i32) {
let new_node = Box::new(Node {
value,
next: self.head.take(),
});
self.head = Some(new_node);
}
pub fn reverse(&mut self) {
// TODO: Implement the reversal logic here.
// Hint: You'll need a variable to track the 'previous' node
// and 'current' node as you traverse.
}
}
fn main() {
let mut list = LinkedList::new();
list.push(1);
list.push(2);
list.push(3);
list.reverse();
// Verification logic: the head should now be 1, then 2, then 3.
let mut current = list.head;
while let Some(node) = current {
println!("Value: {}", node.value);
current = node.next;
}
}
There are no comments for now.