Iterators in Rust
Introduction
Iterators are a powerful and flexible tool in Rust for processing sequences of elements. They provide a way to traverse collections (such as arrays, vectors, and linked lists) while maintaining memory safety and performance.
Core Concepts
What is an Iterator?
An iterator in Rust is defined by the Iterator
trait:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// Many other methods with default implementations
}
Key Principles
- Lazy Evaluation: Iterators don't compute values until requested
- Zero-Cost Abstraction: Iterator operations compile to efficient machine code
- Memory Safety: Iterators respect Rust's ownership and borrowing rules
- Chainable Operations: Multiple iterator adaptors can be combined
- Type Safety: Iterator operations are checked at compile time
Creating Iterators
There are three main ways to create an iterator:
let collection = vec![1, 2, 3, 4, 5];
// 1. Immutable references
let iter = collection.iter(); // &T
// 2. Mutable references
let iter_mut = collection.iter_mut(); // &mut T
// 3. Taking ownership
let into_iter = collection.into_iter(); // T
Basic Usage Example
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Using next() manually
let mut iter = numbers.iter();
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
// Using for loop (more common)
for number in numbers.iter() {
println!("Got: {}", number);
}
}
Iterator Adaptors
Iterator adaptors transform an iterator into another kind of iterator. They are lazy and must be consumed to produce a result.
Common Adaptors
- map: Transform each element
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter()
.map(|x| x * 2)
.collect();
// doubled = [2, 4, 6]
- filter: Keep elements that match a predicate
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter()
.filter(|x| *x % 2 == 0)
.collect();
// evens = [2, 4, 6]
- enumerate: Add indices to elements
let chars = vec!['a', 'b', 'c'];
for (index, &c) in chars.iter().enumerate() {
println!("{}. {}", index, c);
}
- zip: Combine two iterators
let numbers = vec![1, 2, 3];
let letters = vec!['a', 'b', 'c'];
let pairs: Vec<_> = numbers.iter()
.zip(letters.iter())
.collect();
// pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
Consuming Adaptors
These methods consume the iterator and produce a final value.
Common Consumers
- collect: Gather elements into a collection
let numbers = vec![1, 2, 3, 4, 5];
let squares: Vec<i32> = numbers.iter()
.map(|x| x * x)
.collect();
- sum: Add up all elements
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
assert_eq!(sum, 15);
- fold: Accumulate values with custom logic
let numbers = vec![1, 2, 3, 4, 5];
let product = numbers.iter()
.fold(1, |acc, &x| acc * x);
assert_eq!(product, 120); // 5!
Advanced Iterator Patterns
1. Chaining Multiple Operations
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0) // Keep evens
.map(|&x| x * x) // Square them
.filter(|&x| x <= 50) // Keep <= 50
.collect();
// result = [4, 16, 36]
2. Creating Custom Iterators
struct Counter {
count: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Counter {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.count >= self.max {
None
} else {
self.count += 1;
Some(self.count)
}
}
}
// Usage
let counter = Counter::new(3);
for num in counter {
println!("{}", num); // Prints: 1, 2, 3
}
3. Peekable Iterators
let numbers = vec![1, 2, 3, 4, 5];
let mut iter = numbers.iter().peekable();
while let Some(&num) = iter.peek() {
if num % 2 == 0 {
iter.next(); // Skip even numbers
continue;
}
println!("Got odd number: {}", iter.next().unwrap());
}
Best Practices
-
Choose the Right Iterator Method
- Use
iter()
for reading - Use
iter_mut()
for modifying in place - Use
into_iter()
when you need ownership
- Use
-
Leverage Iterator Chains
- Combine operations for cleaner code
- Let the compiler optimize the chains
-
Use Type Inference
// Instead of let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect(); // You can often use let squares = numbers.iter() .map(|x| x * x) .collect::<Vec<_>>();
-
Consider Performance
- Iterators are zero-cost abstractions
- Chain operations before collecting
- Use
filter_map
instead offilter().map()
Common Pitfalls
- Collecting Too Early
// Inefficient
let even: Vec<i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
let doubled: Vec<i32> = even.iter().map(|x| x * 2).collect();
// Better
let result: Vec<i32> = numbers.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
- Forgetting to Collect
// This doesn't do anything!
numbers.iter().map(|x| x * 2);
// Need to collect or consume
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
- Iterator Invalidation
let mut numbers = vec![1, 2, 3];
for i in 0..numbers.len() {
numbers.push(i); // Don't modify while iterating!
}
Next Steps
After mastering iterators:
- Explore the
itertools
crate for additional iterator combinators - Learn about parallel iterators with
rayon
- Study iterator performance optimization
- Understand how iterators work with async code
Remember: Iterators are one of Rust's most powerful features. They combine safety, efficiency, and expressiveness, making them an essential tool for writing idiomatic Rust code.