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
113: Custom Iterator Implementations Revisited
A few years ago, I was reviewing a PR from a junior dev who was building a custom circular buffer for a high-throughput telemetry system. He had the basic Iterator trait implemented, and it worked fine for simple loops. But the moment he tried to use .rev() or .collect::>().reverse(), he realized his iterator only moved forward. When he tried to implement DoubleEndedIterator, he hit a wall: he couldn't figure out how to track the "tail" of the buffer relative to the "head" while maintaining the borrowing rules. He spent two days fighting the borrow checker because he was trying to store indices that he thought were constants, but were actually dynamic offsets.
If you've already implemented a basic iterator, you know the drill: define a struct, implement the Iterator trait, and write the next method. But in production-grade Rust, we rarely need just a simple forward-only stream. We often need iterators that can be consumed from both ends, iterators that don't "restart" unexpectedly, or iterators that tie their lifetime strictly to the collection they are traversing. This is where the "simple" implementation starts to leak.
Tying the Iterator to the Data's Lifetime
The most common stumbling block I see is the relationship between the iterator's lifetime and the data it yields. If your iterator returns references to data owned by a collection, your iterator struct must carry a lifetime parameter. You can't just return &T; you have to return &'a T, where 'a is the lifetime of the source collection.
Take a look at this implementation for a CircularBuffer. Notice how the iterator doesn't own the data; it holds a reference to the buffer. This ensures that the buffer cannot be dropped or mutated while the iterator is still alive.
struct CircularBuffer<T> {
data: Vec<T>,
head: usize,
size: usize,
}
struct CircularBufferIter<'a, T> {
buffer: &'a CircularBuffer<T>,
current: usize,
processed: usize,
}
impl<'a, T> Iterator for CircularBufferIter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if self.processed == self.buffer.size {
return None;
}
// Calculate the actual index in the underlying Vec
let idx = (self.buffer.head + self.current) % self.buffer.data.len();
let item = &self.buffer.data[idx];
self.current += 1;
self.processed += 1;
Some(item)
}
}
I always tell people to pay close attention to type Item = &'a T;. If you omit that 'a, the compiler will scream at you because it has no way of knowing how long that reference is valid. You're explicitly telling Rust: "This item lives as long as the buffer I'm iterating over."
Bidirectional Traversal with DoubleEndedIterator
Now, let's address what my colleague struggled with. To make an iterator work with .rev(), you need the DoubleEndedIterator trait. This requires implementing next_back(). The tricky part is that next_back() must consume elements from the opposite end of the iteration, and it must eventually meet next() in the middle.
In a circular buffer, the "back" is the element immediately preceding the head (wrapping around the end of the vector). To implement this, we need to track how many elements we've consumed from both the front and the back to avoid overlapping.
impl<'a, T> DoubleEndedIterator for CircularBufferIter<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.processed == self.buffer.size {
return None;
}
// The back is the element just before the current 'tail'
// Tail is (head + size) % len, so we go back one from there.
let tail_idx = (self.buffer.head + self.buffer.size + self.buffer.data.len() - 1
- self.current) % self.buffer.data.len();
// Note: This is a simplified mental model. In a real impl,
// you'd need to track 'back_processed' separately from 'current'.
let item = &self.buffer.data[tail_idx];
self.processed += 1;
Some(item)
}
}
It's easy to get the math wrong here. The key is remembering that next() and next_back() are effectively eating away at the same pool of available elements. If you don't decrement a shared counter or track the boundaries carefully, you'll end up yielding the same element twice or panicking on an out-of-bounds index. I usually recommend sketching the indices on a piece of paper before coding the modular arithmetic; it saves a lot of debugging time.
📋 Practical Task
Implement a Bidirectional Sliding Window Iterator
Your task is to create a custom iterator called WindowIter that yields slices of a fixed size from a provided slice, but supports bidirectional iteration.
Requirements:
- Create a struct
WindowIter<'a, T>that holds a reference to a slice&'a [T]and awindow_size: usize. - Implement the
Iteratortrait whereItem` is&'a [T].next()should return the next sliding window (e.g., for a slice[1, 2, 3, 4]and size 2, it should yield[1, 2], then[2, 3], then[3, 4]). - Implement the
DoubleEndedIteratortrait.next_back()should return the windows starting from the end of the slice and moving backwards (e.g.,[3, 4], then[2, 3], then[1, 2]). - Ensure that
next()andnext_back()do not overlap—once an element is yielded by either method, it cannot be yielded again.
Test Case:
let data = [1, 2, 3, 4, 5];
let mut iter = WindowIter::new(&data, 3);
assert_eq!(iter.next(), Some(&data[0..3]));
assert_eq!(iter.next_back(), Some(&data[2..5]));
assert_eq!(iter.next(), Some(&data[1..4]));
assert_eq!(iter.next(), None);
There are no comments for now.