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
114: DoubleEndedIterator and ExactSizeIterator
I've noticed a recurring pattern when I review code from developers moving into Rust: they treat .rev() as if it's a magic method built into the Iterator trait itself. They'll write a custom iterator to handle some complex data traversal, try to call .rev() on it to process the items in reverse, and then get frustrated when the compiler tells them the method doesn't exist.
Stop assuming .rev() is universal
Here is the reality: the Iterator trait only knows how to move forward. If you want to move backward, you need the DoubleEndedIterator trait. The .rev() method is actually provided by a blanket implementation for anything that implements DoubleEndedIterator.
Look at this snippet. I've built a simple LogBuffer that iterates over a slice of strings. If I only implement Iterator, I'm stuck moving in one direction.
struct LogBuffer<'a> {
logs: &'a [String],
index: usize,
}
impl<'a> Iterator for LogBuffer<'a> {
type Item = &'a String;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.logs.len() {
let res = &self.logs[self.index];
self.index += 1;
Some(res)
} else {
None
}
}
}
fn main() {
let logs = vec!["Error 1".to_string(), "Error 2".to_string()];
let mut iter = LogBuffer { logs: &logs, index: 0 };
// This would fail to compile:
// let reversed = iter.rev();
}
To fix this, we don't change next(); we add next_back(). This tells Rust, "I know how to pop elements from the tail just as easily as I can from the head."
impl<'a> DoubleEndedIterator for LogBuffer<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.index < self.logs.len() {
self.index -= 1; // This is a simplification; real logic needs bounds checking
// In a real scenario, you'd track a 'back' index separately
// but for the sake of the point:
Some(&self.logs[self.index])
} else {
None
}
}
}
I'll be honest: implementing DoubleEndedIterator manually can be a bit of a brain-bender because you have to carefully manage your indices so next() and next_back() don't "cross" and return the same element twice. But once you do, .rev() just starts working.
Stop treating size_hint as a promise
The second misconception is about ExactSizeIterator. Many learners think that because Iterator has a size_hint() method, the iterator already "knows" its size. It doesn't. size_hint() returns a (lower, upper) bound. For many iterators (like those filtering a stream), the upper bound is None because we have no clue how many items will actually pass the filter.
If you are collecting an iterator into a Vec, Rust uses size_hint() to allocate memory. If the iterator is just a standard Iterator, the Vec might have to re-allocate and copy its contents multiple times as it grows. This is a performance killer in hot loops.
By implementing ExactSizeIterator, you are making a formal contract with the compiler: "I guarantee that lower == upper."
impl<'a> ExactSizeIterator for LogBuffer<'a> {
// We don't even need to write a method body here!
// Just by implementing this empty trait, we signal that
// our size_hint() is always precise.
}
Now, when you call .collect(), Rust sees the ExactSizeIterator trait, calls size_hint(), and performs exactly one heap allocation for the entire collection. It's a small change that can lead to massive performance gains when dealing with large datasets.
📋 Practical Task
Build a Bidirectional Token Stream
You are building a basic lexer for a custom language. You have a TokenStream that wraps a Vec<Token>. Currently, it can only be processed from start to finish, which makes implementing certain parsing logic (like looking ahead or backtracking) difficult.
Your Task:
- Implement
DoubleEndedIteratorforTokenStreamso that it can be processed in reverse using.rev(). - Implement
ExactSizeIteratorforTokenStreamto ensure that collecting the tokens back into aVecis memory-efficient. - Ensure that
next()andnext_back()do not return the same token; they should meet in the middle and then returnNone.
#[derive(Debug, PartialEq)]
enum Token {
Keyword,
Identifier,
Literal,
}
struct TokenStream {
tokens: Vec<Token>,
front: usize,
back: usize,
}
impl TokenStream {
fn new(tokens: Vec<Token>) -> Self {
let len = tokens.len();
TokenStream {
tokens,
front: 0,
back: len,
}
}
}
impl Iterator for TokenStream {
type Item = Token;
fn next(&mut self) -> Option<Self::Item> {
if self.front < self.back {
let token = self.tokens[self.front].clone(); // simplified for example
self.front += 1;
Some(token)
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.back.saturating_sub(self.front);
(remaining, Some(remaining))
}
}
// TODO: Implement DoubleEndedIterator
// TODO: Implement ExactSizeIterator
fn main() {
let tokens = vec![Token::Keyword, Token::Identifier, Token::Literal];
let stream = TokenStream::new(tokens);
// Test your implementation here:
// 1. Check if .rev() works.
// 2. Check if collecting into a Vec uses the exact size.
}There are no comments for now.