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
76: Implementing a Linked List in Safe Rust
If you're coming from C or Java, you probably think a linked list is the "Hello World" of data structures. In Rust, it's more like the final boss of the ownership system. I've seen plenty of talented engineers hit a wall here because they try to treat Rust pointers like C pointers, only to have the borrow checker scream at them for twenty minutes.
Let's look at a common mistake. You're trying to implement a simple pop method for a singly linked list. You want to take the head node, move the pointer to the next node, and return the value of the old head. It looks intuitive, but it won't compile:
struct Node {
value: i32,
next: Option<Box<Node>>,
}
struct List {
head: Option<Box<Node>>,
}
impl List {
fn pop(&mut self) -> Option<i32> {
// This is where the disaster happens
if self.head.is_some() {
let old_head = self.head.unwrap();
self.head = old_head.next;
Some(old_head.value)
} else {
None
}
}
}
The "Cannot Move Out of Borrowed Content" Wall
If you try to compile this, Rust will throw a fit. The specific error is usually something like cannot move out of self.head which is behind a mutable reference.
Here is what's happening: self.head is inside the List struct, and the pop method takes &mut self. When you call self.head.unwrap(), you're trying to move the Box out of the List. But you don't own the List; you only have a mutable reference to it. Rust refuses to let you leave the List in an invalid state (where head is suddenly empty/undefined) while you're only borrowing the list.
Using Option::take to Swap Ownership
To fix this, we need a way to tell Rust: "I'm taking the value out of this spot, but I'm leaving something (None) in its place immediately." This is exactly what Option::take() is for. It replaces the value at the destination with None and returns the original value.
Here is the corrected pop implementation:
impl List {
fn pop(&mut self) -> Option<i32> {
// .take() pulls the Box out of self.head and leaves None behind
self.head.take().map(|node| {
// Now we own the 'node' Box.
// We put the next node back into the head of the list.
self.head = node.next;
node.value
})
}
}
By using take(), we satisfy the borrow checker. We aren't "stealing" the value and leaving a hole; we're performing a clean swap. Once we have ownership of the node, we can safely move node.next back into self.head before the node itself is dropped at the end of the closure.
Managing the Box Chain
When building a linked list in safe Rust, Box<T> is your best friend for singly linked lists because it provides a fixed size on the stack while the actual data lives on the heap. However, remember that Box implies unique ownership. This is why a doubly linked list is nearly impossible in safe Rust—you can't have two different nodes owning each other with Box.
If you want to implement a push method, you'll follow a similar pattern of ownership transfer. You create a new node, give it ownership of the current head, and then make the new node the new head.
impl List {
fn push(&mut self, value: i32) {
let new_node = Box::new(Node {
value,
// Move the current head into the next field of the new node
next: self.head.take(),
});
// Set the new node as the head
self.head = Some(new_node);
}
}
I personally find it helpful to visualize this as a relay race. The "ownership" baton is being passed from the list to the node, and from the node to the next node. If you try to hold the baton in two places at once, the compiler will stop you.
📋 Practical Task
Exercise: Implementing a Singly Linked List Stack with Length Tracking
Build a Stack data structure based on a singly linked list. Your implementation must include the following:
- A
Nodestruct and aStackstruct. - The
Stackmust track its currentlength: usize. - A
push(val: i32)method that adds an element to the top and increments the length. - A
pop() -> Option<i32>method that removes the top element, decrements the length, and returns the value. - A
peek() -> Option<&i32>method that returns a reference to the top value without removing it.
Requirement: You must use Option::take() in your pop method to handle ownership correctly. Test your stack by pushing three values and popping them all to ensure the length returns to zero.
There are no comments for now.