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
97: LinkedList and When to Avoid It
If you're coming from a Java or C++ background, your instinct when you need a dynamic sequence of elements is often to reach for a linked list. It's the classic computer science answer to "how do I handle a list that grows?" But in Rust, trying to implement a linked list the "traditional" way is a rite of passage that usually ends in a headache. I've seen many developers spend an entire afternoon fighting the borrow checker just to get a basic Node structure working, only to find that their code is slower than if they'd just used a vector.
The fight with the borrow checker
The naive way to build a linked list in Rust is to try and mirror the C-style approach: a struct with a value and a pointer to the next node. You'll likely start with Box<Node>. This works for a simple singly linked list, but the moment you want a doubly linked list—where nodes point both forward and backward—you hit a wall. Rust's ownership rules dictate that a value can have only one owner. A node in a doubly linked list, by definition, is pointed to by both its predecessor and its successor.
// This is the "naive" path that leads to pain
struct Node {
value: i32,
next: Option<Box<Node>>,
prev: Option<Box<Node>>, // Error! This creates a circular ownership loop
}
To fix this, you'll find yourself reaching for Rc<RefCell<T>> to allow multiple ownership and interior mutability. While this technically works, you've now introduced runtime overhead for reference counting and borrow checking. You're no longer writing high-performance Rust; you're essentially simulating a garbage-collected language inside Rust. It's verbose, it's clunky, and it's usually the wrong tool for the job.
Pointer chasing and the cache miss
Beyond the syntax struggle, there's a hardware reality we have to face. Modern CPUs are incredibly fast, but getting data from main memory is slow. CPUs rely on "cache lines"—they grab a chunk of memory and assume that the data you need next is physically located right next to the data you just used. This is why Vec is so dominant. A vector stores its elements contiguously in memory, meaning the CPU can pre-fetch the next few elements with near-perfect accuracy.
A linked list, however, scatters its nodes across the heap. Each time you follow a next pointer, you're potentially jumping to a completely different page of memory. This is "pointer chasing," and it's a performance killer. Even if a linked list theoretically offers $O(1)$ insertion at the head, the constant factor overhead of cache misses often makes a Vec (which might need to occasionally reallocate) faster in practice for everything except the most extreme data sizes.
When a linked list actually makes sense
So, is std::collections::LinkedList useless? Not entirely, but its use case is tiny. You should consider it only if you are doing heavy splitting and merging of lists—operations where you can move a large chunk of data by just swapping a few pointers without iterating through the elements. If you need to push and pop from both ends, VecDeque is almost always the better choice because it uses a growable ring buffer, giving you the best of both worlds: contiguous-ish memory and $O(1)$ operations at both ends.
My rule of thumb is this: start with Vec. If you realize you're doing too many expensive shifts at the front of the list, move to VecDeque. Only if you're implementing a very specific algorithm that requires constant-time splitting or merging should you even look at a linked list. Trust me, your future self—and the borrow checker—will thank you.
📋 Practical Task
Refactoring a Node-Based Undo Stack to a Vector
You have been handed a legacy piece of code for a text editor's "Undo" feature. The previous developer implemented it as a custom singly linked list of Action structs to avoid "expensive" array shifts. However, the current implementation is causing memory fragmentation and is difficult to maintain.
Your Task:
- Rewrite the
UndoStackimplementation. Replace theNodeandBoxlogic with aVec<Action>. - Implement the
push_actionmethod (to add a new action to the stack) and thepop_actionmethod (to retrieve the last action for undoing). - Ensure that the
UndoStackhas a maximum capacity (e.g., 100 actions). If the vector exceeds this size, remove the oldest action (the one at the bottom of the stack) to make room for the new one. - Compare the mental overhead: notice how the ownership logic simplifies once the
Boxpointers are gone.
There are no comments for now.