Rust’s iterator system is one of its most powerful features. Iterators are lazy, composable, and compile to code that is just as fast as hand-written loops — often faster, because the compiler can better reason about the intent. Everything from standard library collections to custom data structures participates in the same unified Iterator trait.
The Iterator Trait
All iterators implement a single trait with one required method:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 70+ default methods built on top of next()
}
When next() returns Some(item), iteration continues. When it returns None, iteration ends. The for loop is syntactic sugar for calling next() in a loop:
fn main() {
let v = vec![10, 20, 30];
// These two are identical:
for x in &v {
println!("{}", x);
}
let mut iter = v.iter();
while let Some(x) = iter.next() {
println!("{}", x);
}
}
The Three Iterator Methods on Collections
Every collection gives you three ways to get an iterator, with different ownership semantics:
fn main() {
let words = vec!["hello", "world", "rust"];
// iter() — borrows immutably, yields &T
for word in words.iter() {
println!("{}", word); // word: &&str
}
println!("words still available: {:?}", words);
// iter_mut() — borrows mutably, yields &mut T
let mut numbers = vec![1, 2, 3, 4, 5];
for n in numbers.iter_mut() {
*n *= 10;
}
println!("{:?}", numbers); // [10, 20, 30, 40, 50]
// into_iter() — takes ownership, yields T
let owned = vec![String::from("a"), String::from("b")];
for s in owned.into_iter() {
println!("Owned: {}", s);
}
// `owned` is consumed — no longer accessible
}
Using for x in collection without calling a method calls into_iter() implicitly.
Iterator Adapters
Adapters transform one iterator into another. They are lazy — no work happens until a consuming method is called. This lets you build complex pipelines with zero intermediate allocations.
map — Transform Each Element
fn main() {
let prices = vec![9.99, 14.99, 4.99];
let with_tax: Vec<f64> = prices.iter()
.map(|&p| (p * 1.08 * 100.0).round() / 100.0)
.collect();
println!("{:?}", with_tax); // [10.79, 16.19, 5.39]
}
filter — Keep Matching Elements
fn main() {
let users = vec![
("alice", true),
("bob", false),
("carol", true),
];
let active: Vec<&str> = users.iter()
.filter(|(_, active)| *active)
.map(|(name, _)| *name)
.collect();
println!("{:?}", active); // ["alice", "carol"]
}
filter_map — Filter and Transform in One Step
Avoids a nested filter + map when the transform can fail:
fn main() {
let raw = vec!["42", "not_a_number", "17", "", "99"];
let numbers: Vec<i32> = raw.iter()
.filter_map(|s| s.parse().ok())
.collect();
println!("{:?}", numbers); // [42, 17, 99]
}
flat_map — Map and Flatten
fn main() {
let sentences = vec!["hello world", "rust is great"];
let words: Vec<&str> = sentences.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", words); // ["hello", "world", "rust", "is", "great"]
}
enumerate — Index + Value
fn main() {
let fruits = ["apple", "banana", "cherry"];
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// 0: apple, 1: banana, 2: cherry
}
zip — Pair Two Iterators
fn main() {
let names = ["Alice", "Bob", "Carol"];
let scores = [92, 88, 95];
let leaderboard: Vec<(&str, i32)> = names.iter()
.copied()
.zip(scores.iter().copied())
.collect();
println!("{:?}", leaderboard);
// [("Alice", 92), ("Bob", 88), ("Carol", 95)]
}
take and skip
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Pagination: page 2, page size 3
let page_size = 3;
let page = 1; // 0-indexed
let page_items: Vec<_> = data.iter()
.skip(page * page_size)
.take(page_size)
.collect();
println!("{:?}", page_items); // [4, 5, 6]
}
chain — Concatenate Two Iterators
fn main() {
let first = vec![1, 2, 3];
let second = vec![4, 5, 6];
let combined: Vec<i32> = first.iter()
.chain(second.iter())
.copied()
.collect();
println!("{:?}", combined); // [1, 2, 3, 4, 5, 6]
}
peekable — Look Ahead Without Consuming
fn main() {
let mut iter = vec![1, 2, 3].into_iter().peekable();
while let Some(&next) = iter.peek() {
if next > 2 {
break;
}
println!("{}", iter.next().unwrap());
}
// Prints 1, 2
}
windows and chunks — Slice-Based Iteration
fn main() {
let data = vec![1, 2, 3, 4, 5];
// Sliding window of size 3
for window in data.windows(3) {
let sum: i32 = window.iter().sum();
println!("{:?} → sum {}", window, sum);
}
// [1, 2, 3] → sum 6
// [2, 3, 4] → sum 9
// [3, 4, 5] → sum 12
// Non-overlapping chunks
for chunk in data.chunks(2) {
println!("{:?}", chunk);
}
// [1, 2], [3, 4], [5]
}
Consuming Adaptors
These methods drive the iterator to completion and produce a final value.
collect
The most versatile consumer — turns an iterator into any collection:
use std::collections::{HashMap, HashSet};
fn main() {
let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
// Into HashMap
let map: HashMap<&str, i32> = pairs.into_iter().collect();
println!("{:?}", map);
// Into HashSet (deduplication)
let dupes = vec![1, 2, 2, 3, 3, 3];
let unique: HashSet<i32> = dupes.into_iter().collect();
println!("{:?}", unique); // {1, 2, 3}
// Join strings
let words = vec!["one", "two", "three"];
let joined: String = words.join(", ");
println!("{}", joined); // one, two, three
}
fold and reduce
fold accumulates a result with an initial value; reduce uses the first element as the initial value:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// fold: build a product
let product = numbers.iter().fold(1i64, |acc, &x| acc * x as i64);
println!("{}", product); // 120
// reduce: max value
let max = numbers.iter().copied().reduce(|a, b| if a > b { a } else { b });
println!("{:?}", max); // Some(5)
// Building a string with fold
let csv = numbers.iter()
.fold(String::new(), |mut acc, &n| {
if !acc.is_empty() { acc.push(','); }
acc.push_str(&n.to_string());
acc
});
println!("{}", csv); // 1,2,3,4,5
}
any and all
Short-circuit boolean checks:
fn main() {
let values = vec![2, 4, 6, 7, 8];
println!("{}", values.iter().all(|&x| x % 2 == 0)); // false (7 is odd)
println!("{}", values.iter().any(|&x| x % 2 != 0)); // true (7)
// Useful for validation
let emails = vec!["[email protected]", "invalid", "[email protected]"];
let all_valid = emails.iter().all(|e| e.contains('@'));
println!("All valid: {}", all_valid); // false
}
find and position
fn main() {
let users = vec![
(1u32, "Alice"),
(2, "Bob"),
(3, "Carol"),
];
let found = users.iter().find(|(id, _)| *id == 2);
println!("{:?}", found); // Some((2, "Bob"))
let pos = users.iter().position(|(_, name)| *name == "Carol");
println!("{:?}", pos); // Some(2)
}
sum and product
fn main() {
let v = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let sum: f64 = v.iter().sum();
let product: f64 = v.iter().product();
println!("sum={}, product={}", sum, product); // sum=15, product=120
}
Implementing a Custom Iterator
Implement Iterator for any type by providing next(). All 70+ standard adapters become available automatically.
A Range-Step Iterator
struct StepRange {
current: i32,
end: i32,
step: i32,
}
impl StepRange {
fn new(start: i32, end: i32, step: i32) -> Self {
StepRange { current: start, end, step }
}
}
impl Iterator for StepRange {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
let val = self.current;
self.current += self.step;
Some(val)
} else {
None
}
}
}
fn main() {
let evens: Vec<i32> = StepRange::new(0, 20, 2).collect();
println!("{:?}", evens); // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
// Free adapters from implementing Iterator
let sum: i32 = StepRange::new(1, 10, 2).sum();
println!("Sum of odds 1-9: {}", sum); // 25
}
A Tree Node Iterator (Depth-First)
#[derive(Debug)]
struct TreeNode {
value: i32,
children: Vec<TreeNode>,
}
struct DfsIterator {
stack: Vec<TreeNode>,
}
impl DfsIterator {
fn new(root: TreeNode) -> Self {
DfsIterator { stack: vec![root] }
}
}
impl Iterator for DfsIterator {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
let node = self.stack.pop()?;
// Push children in reverse order for correct DFS order
for child in node.children.into_iter().rev() {
self.stack.push(child);
}
Some(node.value)
}
}
fn main() {
let tree = TreeNode {
value: 1,
children: vec![
TreeNode { value: 2, children: vec![
TreeNode { value: 4, children: vec![] },
TreeNode { value: 5, children: vec![] },
]},
TreeNode { value: 3, children: vec![
TreeNode { value: 6, children: vec![] },
]},
],
};
let values: Vec<i32> = DfsIterator::new(tree).collect();
println!("{:?}", values); // [1, 2, 4, 5, 3, 6]
}
Performance: Zero-Cost Abstraction
Iterator chains compile down to the same machine code as hand-written loops. The Rust compiler aggressively inlines and unrolls iterator chains. In many cases iterators are faster than loops because they express intent clearly, enabling better auto-vectorization.
fn sum_of_squares_loop(data: &[f64]) -> f64 {
let mut sum = 0.0;
for &x in data {
sum += x * x;
}
sum
}
fn sum_of_squares_iter(data: &[f64]) -> f64 {
data.iter().map(|&x| x * x).sum()
}
// Both compile to identical (or near-identical) machine code.
// The iterator version often enables SIMD auto-vectorization.
Parallel Iteration with Rayon
For CPU-bound work, the rayon crate adds a par_iter() method that distributes work across threads automatically — no manual thread management:
// Cargo.toml: rayon = "1.10"
use rayon::prelude::*;
fn main() {
let data: Vec<u64> = (0..1_000_000).collect();
// Parallel sum — uses all CPU cores automatically
let sum: u64 = data.par_iter().sum();
println!("{}", sum);
// Parallel filter + map
let result: Vec<u64> = data.par_iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("Even squares count: {}", result.len());
}
The API is identical to the sequential iterator API — just change .iter() to .par_iter().
Common Patterns
Grouping with HashMap
use std::collections::HashMap;
fn main() {
let words = vec!["apple", "ant", "bear", "avocado", "bat", "cherry"];
let grouped: HashMap<char, Vec<&str>> = words.iter()
.fold(HashMap::new(), |mut map, &word| {
map.entry(word.chars().next().unwrap())
.or_default()
.push(word);
map
});
for (letter, words) in &grouped {
println!("{}: {:?}", letter, words);
}
}
Early Exit with try_fold
When your fold can fail, use try_fold:
fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs.iter().try_fold(Vec::new(), |mut acc, s| {
acc.push(s.parse::<i32>()?);
Ok(acc)
})
}
fn main() {
println!("{:?}", parse_all(&["1", "2", "3"])); // Ok([1, 2, 3])
println!("{:?}", parse_all(&["1", "bad", "3"])); // Err(...)
}
Summary
| Method | Type | Description |
|---|---|---|
map |
Adapter | Transform each element |
filter |
Adapter | Keep elements matching predicate |
filter_map |
Adapter | Transform, drop None results |
flat_map |
Adapter | Map + flatten one level |
enumerate |
Adapter | Pair with index |
zip |
Adapter | Pair two iterators |
take / skip |
Adapter | Limit or offset iteration |
chain |
Adapter | Concatenate two iterators |
peekable |
Adapter | Non-consuming lookahead |
collect |
Consumer | Gather into a collection |
fold |
Consumer | Reduce with accumulator |
any / all |
Consumer | Short-circuit boolean check |
find / position |
Consumer | Search |
sum / product |
Consumer | Aggregate numerics |
The key insight: iterators express what you want to compute, not how. The compiler figures out the most efficient implementation, and you get readable, composable code with no overhead.
Comments