20 - Iterators
📋 Jump to Takeaways🎁 What if you could describe a data transformation pipeline, filter this, transform that, take the first five, and the compiler would fuse it all into a single tight loop with no temporary allocations? No intermediate vectors, no heap overhead, just one pass through the data.
The Iterator Trait
The Iterator trait has one required method: next(), which returns Option<Self::Item> (Item is the type of element this iterator produces, each iterator defines this).
fn main() {
let nums = vec![10, 20, 30];
let mut iter = nums.iter();
println!("{:?}", iter.next()); // Some(10)
println!("{:?}", iter.next()); // Some(20)
println!("{:?}", iter.next()); // Some(30)
println!("{:?}", iter.next()); // None
}Iterators are lazy, they do nothing until you consume them. This is what enables the compiler to fuse the entire chain into a single loop.
Creating Iterators
You have three ways to create an iterator from a collection:
.iter(), iterates over&T(immutable references).iter_mut(), iterates over&mut T(mutable references).into_iter(), iterates overT(takes ownership)
fn main() {
let mut names = vec!["Alice", "Bob", "Carol"];
// .iter() — borrows, names stays usable
for name in names.iter() {
println!("{}", name);
}
// .iter_mut() — borrows mutably, can modify in place
for name in names.iter_mut() {
*name = "Modified";
}
println!("{:?}", names); // ["Modified", "Modified", "Modified"] — still usable
// .into_iter() — moves each element out, names is gone after this
for name in names.into_iter() {
println!("got ownership of: {}", name);
}
// println!("{:?}", names); // ERROR — names was consumed by into_iter()
}Iterator Adaptors
Adaptors transform an iterator into another iterator. They're lazy — nothing happens until you consume the result. Think of them as building a pipeline. No work runs yet.
Adaptors (lazy, return another iterator — chain as many as you want):
.map(), .filter(), .enumerate(), .zip(), .skip(), .take(), .chain()
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// .map — transform each element
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// .filter — keep elements matching a predicate
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6, 8, 10]
// .enumerate — attach index to each element
for (i, val) in numbers.iter().enumerate().take(3) {
println!("index {}: {}", i, val); // index 0: 1, index 1: 2, index 2: 3
}
// .zip — pair elements from two iterators
let letters = vec!['a', 'b', 'c'];
// The _ tells Rust to infer the element type — you're specifying
// it's a Vec but letting the compiler figure out what's inside
let zipped: Vec<_> = numbers.iter().zip(letters.iter()).collect();
println!("{:?}", zipped); // [(1, 'a'), (2, 'b'), (3, 'c')]
// .skip and .chain
let skipped: Vec<&i32> = numbers.iter().skip(7).collect();
println!("{:?}", skipped); // [8, 9, 10]
let extra = vec![11, 12];
let chained: Vec<&i32> = numbers.iter().chain(extra.iter()).skip(8).collect();
println!("{:?}", chained); // [9, 10, 11, 12]
}Consumers
Consumers drive the iterator and produce a final value. They pull every element through the pipeline and collapse it into a result. Once consumed, the iterator is gone.
Consumers (eager, end the chain and return a value — pick one):
.collect(), .sum(), .product(), .count(), .any(), .all(), .find(), .position(), .fold()
Consumers are methods on the Iterator trait, not on Vec or other collections directly. You always need .iter(), .iter_mut(), or .into_iter() first:
let total: i32 = numbers.sum(); // ERROR — Vec doesn't have .sum()
let total: i32 = numbers.iter().sum(); // ✅fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
println!("sum: {}", total); // sum: 15
// .iter() yields &i32, and .filter() passes each item by reference again,
// giving you &&i32. Writing |&&x| destructures both layers to get the plain i32 value.
let count = numbers.iter().filter(|&&x| x > 2).count();
println!("count > 2: {}", count); // count > 2: 3
// Some operators like % work on references automatically. Comparison
// operators like > with literals need explicit * to dereference.
// When in doubt, the compiler will tell you.
let has_even = numbers.iter().any(|x| x % 2 == 0);
println!("has even: {}", has_even); // has even: true
let all_positive = numbers.iter().all(|x| *x > 0);
println!("all positive: {}", all_positive); // all positive: true
// .find — returns the first element matching a condition, or None
let first_big = numbers.iter().find(|x| **x > 3);
println!("first > 3: {:?}", first_big); // first > 3: Some(4)
// .fold — like .sum() but you control how elements combine.
// First argument is the starting value, closure gets (accumulated, current).
let product = numbers.iter().fold(1, |acc, x| acc * x);
// step by step: 1*1=1, 1*2=2, 2*3=6, 6*4=24, 24*5=120
println!("product: {}", product); // product: 120
}Method Chaining
The real power emerges when you chain multiple adaptors together. Each step is clear, composable, and zero-cost.
fn main() {
let words = vec!["hello", "world", "rust", "is", "fast"];
let result: String = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect::<Vec<String>>()
.join(", ");
println!("{}", result); // HELLO, WORLD, RUST, FAST
}.collect() needs to know what collection type to build. You have two ways to tell it:
// Option 1: annotate the variable — collect() figures it out
let v: Vec<String> = words.iter().map(|w| w.to_uppercase()).collect();
// Option 2: turbofish — put the type on collect() itself
let v = words.iter().map(|w| w.to_uppercase()).collect::<Vec<String>>();Both do the same thing. Turbofish (::<>) is useful when you can't annotate the variable, like when returning a value inline from a function.
You can read this top to bottom: take the words, keep those longer than 3 characters, uppercase them, collect into a vector, join with commas.
Zero-Cost Abstraction
When you write .filter().map().sum(), you might expect Rust to build a temporary vector after each step. It doesn't. The compiler sees the whole chain at once, inlines each closure, and merges everything into a single loop — the same machine code you'd write by hand.
In most languages, chaining methods means paying for the convenience: extra allocations, virtual function calls, or multiple passes over the data. In Rust, you pay nothing. That's what "zero-cost abstraction" means: the readable version and the hand-optimized version are identical after compilation.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// This iterator chain...
let sum_a: i32 = numbers.iter().filter(|&&x| x % 2 == 0).map(|x| x * x).sum();
// ...compiles to the same machine code as this loop:
// &numbers is shorthand for .iter(), and &x in the pattern destructures
// the reference so x is the value directly.
let mut sum_b = 0;
for &x in &numbers {
if x % 2 == 0 {
sum_b += x * x;
}
}
println!("{} == {}", sum_a, sum_b); // 220 == 220
}The iterator version is more readable, more composable, and equally fast. This is what "zero-cost abstraction" means in Rust.
Key Takeaways
- Iterators are lazy, adaptors build a pipeline, consumers execute it
.iter()borrows,.iter_mut()borrows mutably,.into_iter()consumes- Chain adaptors like
.map(),.filter(),.enumerate(),.zip()freely - Consumers like
.collect(),.sum(),.fold(),.find()drive execution .collect()needs a type hint (annotation or turbofish) to know what to build- Iterator chains compile to the same machine code as manual loops, zero-cost abstraction
- No intermediate allocations between adaptor steps, the compiler fuses the chain
🎁 You've called println!, vec!, and assert! throughout this course — all with a ! suffix. That's not decoration. Next up: what macros actually are, why they exist, and how to write your own.