Iterators
Iterators are Rust's abstraction for sequential processing. They are lazy, composable, and zero-cost — iterator chains compile down to the same machine code as hand-written loops. Once you internalize the iterator API, you will write less code, produce fewer bugs, and often get better performance than manual loops.
The Three Iterator Types
Every collection in Rust can produce three kinds of iterators. Understanding the difference is essential:
fn main() {
let names = vec![
String::from("Alice"),
String::from("Bob"),
String::from("Charlie"),
];
// .iter() — borrows each element as &T
for name in names.iter() {
println!("Hello, {}", name); // name is &String
}
// names is still valid here
// .iter_mut() — borrows each element as &mut T
let mut numbers = vec![1, 2, 3, 4, 5];
for n in numbers.iter_mut() {
*n *= 2; // modify in place
}
println!("Doubled: {:?}", numbers);
// .into_iter() — takes ownership, consumes the collection
let names2 = vec![
String::from("Dave"),
String::from("Eve"),
];
for name in names2.into_iter() {
println!("Consumed: {}", name); // name is String (owned)
}
// names2 is no longer valid — it was consumed
}
Hello, Alice
Hello, Bob
Hello, Charlie
Doubled: [2, 4, 6, 8, 10]
Consumed: Dave
Consumed: Eve
The for loop calls .into_iter() by default. Writing for x in &collection calls .iter(), and for x in &mut collection calls .iter_mut().
map, filter, fold
These are the building blocks of iterator processing:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: transform each element
let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
println!("Squares: {:?}", squares);
// filter: keep elements matching a predicate
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
println!("Evens: {:?}", evens);
// fold: accumulate into a single value
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {}", sum);
// Chaining: filter then map
let even_squares: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("Even squares: {:?}", even_squares);
}
Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Evens: [2, 4, 6, 8, 10]
Sum: 55
Even squares: [4, 16, 36, 64, 100]
Iterator Chains Are Zero-Cost
This is the critical insight. Iterator chains do not create intermediate collections. They compile to the same code as a manual loop:
fn manual_loop(data: &[i32]) -> i32 {
let mut sum = 0;
for &x in data {
if x > 0 {
sum += x * x;
}
}
sum
}
fn iterator_chain(data: &[i32]) -> i32 {
data.iter()
.filter(|&&x| x > 0)
.map(|&x| x * x)
.sum()
}
fn main() {
let data = vec![-3, -1, 0, 2, 4, -5, 7];
println!("Manual: {}", manual_loop(&data));
println!("Iterator: {}", iterator_chain(&data));
}
Manual: 69
Iterator: 69
In release mode, the compiler optimizes both functions to identical machine code. The iterator version is often preferred because it is more declarative and harder to mess up.
Lazy Evaluation
Iterators are lazy. Nothing happens until you consume them:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// This does nothing — the iterator is created but never consumed
let _lazy = numbers.iter().map(|x| {
println!("Processing {}", x); // never printed
x * 2
});
println!("Before collect:");
// Now it runs — collect() consumes the iterator
let doubled: Vec<i32> = numbers.iter().map(|&x| {
println!("Processing {}", x);
x * 2
}).collect();
println!("Result: {:?}", doubled);
}
Before collect:
Processing 1
Processing 2
Processing 3
Processing 4
Processing 5
Result: [2, 4, 6, 8, 10]
Laziness means you can build complex pipelines without wasting memory on intermediate results.
Essential Iterator Methods
fn main() {
let data = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
// any / all — short-circuit boolean checks
let has_negative = data.iter().any(|&x| x < 0);
let all_positive = data.iter().all(|&x| x > 0);
println!("Has negative: {}, All positive: {}", has_negative, all_positive);
// find — first matching element
let first_big = data.iter().find(|&&x| x > 5);
println!("First > 5: {:?}", first_big);
// position — index of first match
let pos = data.iter().position(|&x| x == 9);
println!("Position of 9: {:?}", pos);
// take / skip — slice the iterator
let first_three: Vec<_> = data.iter().take(3).collect();
let after_five: Vec<_> = data.iter().skip(5).collect();
println!("First 3: {:?}", first_three);
println!("After 5: {:?}", after_five);
// enumerate — index + value pairs
for (i, val) in data.iter().enumerate().take(4) {
println!(" [{}] = {}", i, val);
}
// zip — pair elements from two iterators
let keys = vec!["a", "b", "c"];
let vals = vec![1, 2, 3];
let pairs: Vec<_> = keys.iter().zip(vals.iter()).collect();
println!("Pairs: {:?}", pairs);
// chain — concatenate iterators
let first = vec![1, 2];
let second = vec![3, 4];
let combined: Vec<_> = first.iter().chain(second.iter()).collect();
println!("Combined: {:?}", combined);
// flat_map — map then flatten
let sentences = vec!["hello world", "foo bar baz"];
let words: Vec<&str> = sentences.iter().flat_map(|s| s.split_whitespace()).collect();
println!("Words: {:?}", words);
}
Has negative: false, All positive: true
First > 5: Some(9)
Position of 9: Some(5)
First 3: [3, 1, 4]
After 5: [9, 2, 6, 5, 3, 5]
[0] = 3
[1] = 1
[2] = 4
[3] = 1
Pairs: [("a", 1), ("b", 2), ("c", 3)]
Combined: [1, 2, 3, 4]
Words: ["hello", "world", "foo", "bar", "baz"]
collect: The Universal Consumer
collect transforms an iterator into any collection that implements FromIterator:
use std::collections::{HashMap, HashSet, BTreeMap};
fn main() {
let data = vec![("alice", 95), ("bob", 87), ("charlie", 92), ("alice", 88)];
// Into a HashMap (last value wins for duplicate keys)
let scores: HashMap<&str, i32> = data.iter().cloned().collect();
println!("Scores: {:?}", scores);
// Into a HashSet (unique values only)
let names: HashSet<&str> = data.iter().map(|(name, _)| *name).collect();
println!("Names: {:?}", names);
// Into a BTreeMap (sorted)
let sorted: BTreeMap<&str, i32> = data.iter().cloned().collect();
println!("Sorted: {:?}", sorted);
// Collect Results — stops at first error
let numbers: Result<Vec<i32>, _> = vec!["1", "2", "three", "4"]
.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("Parsed: {:?}", numbers);
// String from chars
let greeting: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
println!("Uppercased: {}", greeting);
}
Scores: {"bob": 87, "alice": 88, "charlie": 92}
Names: {"charlie", "alice", "bob"}
Sorted: {"alice": 88, "bob": 87, "charlie": 92}
Parsed: Err(invalid digit found in string)
Uppercased: HELLO
Real-World Example: Log Processing
#[derive(Debug)]
struct LogEntry {
level: String,
message: String,
timestamp: u64,
}
fn parse_log_line(line: &str) -> Option<LogEntry> {
let parts: Vec<&str> = line.splitn(3, ' ').collect();
if parts.len() < 3 {
return None;
}
Some(LogEntry {
timestamp: parts[0].parse().ok()?,
level: parts[1].to_string(),
message: parts[2].to_string(),
})
}
fn main() {
let raw_logs = "\
1000 INFO Server started
1001 DEBUG Connection from 192.168.1.1
1002 ERROR Failed to read database
1003 INFO Request processed in 42ms
1004 WARN Slow query detected
1005 ERROR Connection timeout
1006 INFO Shutting down";
let entries: Vec<LogEntry> = raw_logs
.lines()
.filter_map(parse_log_line)
.collect();
let error_count = entries.iter().filter(|e| e.level == "ERROR").count();
let last_error = entries.iter().rev().find(|e| e.level == "ERROR");
println!("Total entries: {}", entries.len());
println!("Errors: {}", error_count);
if let Some(err) = last_error {
println!("Last error: {} (t={})", err.message, err.timestamp);
}
// Group by level
let mut by_level: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for entry in &entries {
*by_level.entry(&entry.level).or_insert(0) += 1;
}
let mut sorted: Vec<_> = by_level.iter().collect();
sorted.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
println!("\nBy level:");
for (level, count) in sorted {
println!(" {}: {}", level, count);
}
}
Total entries: 7
Errors: 2
Last error: Connection timeout (t=1005)
By level:
INFO: 3
ERROR: 2
DEBUG: 1
WARN: 1
Common Pitfalls
- Forgetting that iterators are lazy.
vec.iter().map(|x| do_something(x))does nothing without a consumer likecollect(),for_each(),count(), orsum(). - Using
collectwhen you only need one value. If you want the first match, usefind, notfilter().collect()followed by indexing. - Indexing instead of iterating.
for i in 0..vec.len() { vec[i] }is less idiomatic and less efficient thanfor item in &vec. Bounds checks are eliminated when the compiler knows you are iterating the whole collection. - Creating intermediate collections unnecessarily. Chain iterator methods instead of collecting into a
Vecjust to iterate again. - Confusing
iter()andinto_iter().iter()borrows.into_iter()consumes. If you need the collection after iteration, useiter(). - Not using
filter_mapfor combined filter + map. When your map function returnsOption,filter_maphandles both steps cleanly.
Key Takeaways
.iter()borrows,.iter_mut()borrows mutably,.into_iter()consumes.- Iterator chains compile to the same code as hand-written loops. Zero overhead.
- Iterators are lazy. Nothing executes until a consumer (
.collect(),.sum(),.count(), etc.) drives the chain. collectcan produce any collection type that implementsFromIterator.- Prefer iterator chains over manual loops. They are more readable, more composable, and equally fast.
- Use
filter_mapfor "try to transform, skip failures" andflat_mapfor "transform into sequences, then flatten."