Iterators
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and iterators let you process sequences lazily and expressively, with zero runtime cost.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll chain adapters like map and filter, finish with consumers like collect and sum, and understand laziness.
What You'll Learn in This Lesson
- Creating iterators with iter() and ranges
- Transforming with map and filter
- Consuming with collect, sum, and count
- Why iterators are lazy
- Finding and enumerating elements
- iter vs iter_mut vs into_iter
1️⃣ Adapters: map, filter, and collect
Call .iter() on a collection to get an iterator, then chain adapters that transform it: filter keeps matching items, map transforms each one. Adapters are lazy — they don't run until a consumer like collect or sum drives the chain.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
// Iterator ADAPTERS transform an iterator and are LAZY — nothing happens
// until a CONSUMER like collect() or sum() runs the chain.
let result: Vec<i32> = numbers
.iter() // borrow each element
.filter(|&&x| x % 2 == 0)// keep only even numbers
.map(|&x| x * 10) // multiply each by 10
.collect(); // CONSUME into a new Vec
println!("{:?}", result); // -> [20, 40, 60]
// sum() is a consumer that adds everything up.
let total: i32 = numbers.iter().sum();
println!("total: {}", total); // -> total: 21
// map + collect to build a new transformed vector.
let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();
println!("squares: {:?}", squares); // -> [1, 4, 9, 16, 25, 36]
}The whole filter().map() pipeline did nothing until collect() ran it. That laziness is what makes long chains efficient — no throwaway intermediate vectors.
2️⃣ Consumers: count, find, enumerate, and Ranges
Beyond collect, many consumers produce a single value: count, max, find (returns an Option). The enumerate adapter pairs each item with its index. And ranges like 1..=4 are iterators themselves.
fn main() {
let words = vec!["apple", "banana", "cherry", "date"];
// count, max, min, and find are consumers that produce a single value.
let long_words = words.iter().filter(|w| w.len() > 5).count();
println!("words longer than 5: {}", long_words); // -> 2
// find returns the first match as an Option.
let first_b = words.iter().find(|w| w.starts_with('b'));
println!("first b-word: {:?}", first_b); // -> Some("banana")
// enumerate pairs each item with its index.
for (i, w) in words.iter().enumerate() {
println!("{}: {}", i, w);
}
// Ranges are iterators too. Sum the squares of 1..=4.
let sum_squares: i32 = (1..=4).map(|n| n * n).sum();
println!("sum of squares 1..=4: {}", sum_squares); // -> 30
}Notice find returned an Option — there might be no match. And summing the squares of a range showed how adapters and consumers compose over anything iterable.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the iterator chain, then run it.
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];
// 1) Keep only the ODD numbers, then double them, then collect to a Vec.
let result: Vec<i32> = nums
.iter()
.filter(|&&x| x % 2 == ___) // 👉 odd numbers have remainder 1
.map(|&x| x * 2)
.___(); // 👉 the consumer that builds a Vec
println!("{:?}", result); // expected: [2, 6, 10, 14]
// 2) Sum all the numbers.
let total: i32 = nums.iter().___(); // 👉 the consumer that adds them up
println!("total = {}", total); // expected: total = 36
}
// ✅ Expected output:
// [2, 6, 10, 14]
// total = 36Mini-Challenge: Process Scores
Use adapters and consumers to count, find the max, and filter a list of scores. Run it with cargo run.
// 🎯 MINI-CHALLENGE: process a list of scores.
//
// Given let scores = vec![45, 80, 30, 95, 60, 100];
// 1. Count how many scores are passing (>= 50). Use filter + count.
// 2. Find the highest score with .iter().max() (handle the Option with
// unwrap_or, or copied().max()).
// 3. Build a Vec of only the passing scores, each labelled by adding 0 (just
// collect the passing ones into a Vec<i32>).
// 4. Print the count, the max, and the passing vector.
//
// ✅ Example output:
// Passing: 4
// Highest: 100
// Passing scores: [80, 95, 60, 100]
fn main() {
// your code here
}Common Errors (and the fix)
- Nothing happens / "unused `Map` that must be used" — You built a chain but never consumed it. Add a consumer like .collect() or .sum().
- "type annotations needed" on collect — Tell collect what to build: let v: Vec<i32> = ...collect();.
- "cannot multiply `&i32` by `i32`" — In .iter() chains items are references. Dereference with |&x| x * 2 or use .copied().
- Double reference in filter — filter gives a reference to the item, so over an .iter() you often pattern-match |&&x|.
Pro Tips
- 💡 One consumer per chain: end with exactly one consumer; the adapters before it are lazy.
- 💡 Use copied() or cloned(): they turn an iterator of references into one of values, simplifying closures.
- 💡 Iterators are zero-cost: idiomatic chains compile to the same speed as hand-written loops.
📋 Quick Reference — Iterators
| Goal | Rust Syntax |
|---|---|
| Iterate | v.iter() |
| Transform | .map(|x| x * 2) |
| Keep some | .filter(|x| cond) |
| Build a Vec | .collect::<Vec<_>>() |
| Add up | .sum::<i32>() |
| Index pairs | .enumerate() |
🎉 Lesson Complete!
- ✅ Adapters like map and filter are lazy and chainable
- ✅ Consumers like collect, sum, and count run the chain
- ✅ find returns an Option; enumerate pairs items with indexes
- ✅ Iterators are zero-cost — as fast as hand-written loops
Practice quiz
What does it mean that iterator adapters are lazy?
- They run immediately
- They only work on numbers
- They do no work until a consumer drives the chain
- They cache results
Answer: They do no work until a consumer drives the chain. Adapters build a description; nothing runs until a consumer pulls values.
Which of these is an iterator adapter?
- map
- collect
- sum
- count
Answer: map. map returns a new iterator, so it is an adapter, not a consumer.
Which of these is a consumer?
- filter
- map
- enumerate
- collect
Answer: collect. collect runs the chain to completion and builds a final collection.
What does .filter() do?
- Transforms each item
- Keeps only items matching a condition
- Sums items
- Reverses the order
Answer: Keeps only items matching a condition. filter keeps only the elements for which the closure returns true.
What does .map() do?
- Transforms each item
- Keeps some items
- Counts items
- Sorts items
Answer: Transforms each item. map applies a closure to transform every element.
What does .collect() do?
- Adds numbers
- Finds the max
- Builds a collection from the iterator
- Prints items
Answer: Builds a collection from the iterator. collect consumes the iterator into a collection like a Vec.
What does .find() return?
- A Vec
- An Option
- A bool
- The index
Answer: An Option. find returns the first matching item as an Option (Some or None).
What does .enumerate() produce?
- Only values
- Only indexes
- A single count
- Pairs of (index, item)
Answer: Pairs of (index, item). enumerate pairs each item with its position index.
How do you typically end an iterator chain?
- With many consumers
- With exactly one consumer
- With no consumer
- With a semicolon only
Answer: With exactly one consumer. A chain is several adapters followed by exactly one consumer.
How fast are idiomatic iterator chains compared to hand-written loops?
- Much slower
- Slightly slower always
- Just as fast (zero-cost)
- Only fast in release with unsafe
Answer: Just as fast (zero-cost). Iterators are a zero-cost abstraction, compiling to loop-equivalent code.
Continue this course
- Previous: Closures
- Next: Smart Pointers (Box, Rc, RefCell) — Heap allocation, shared ownership, and interior mutability
- Quick reference: Rust cheat sheet › Collections
Frequently asked questions
What does it mean that iterators are lazy?
Iterator adapters like map and filter do no work when you call them; they just build up a description of the computation. Nothing actually runs until you call a consumer such as collect, sum, count, or a for loop, which pulls values through the whole chain. This laziness means you can chain many transformations efficiently without creating intermediate collections at each step, and you can even work with infinite iterators as long as something eventually stops pulling.
What is the difference between an adapter and a consumer?
An iterator adapter takes an iterator and returns a new iterator, transforming the sequence — examples include map, filter, take, skip, and enumerate. Because they return iterators, adapters can be chained together. A consumer (also called a consuming adapter) is a method that runs the iterator to completion and produces a final value or collection — examples include collect, sum, count, max, find, and for_each. A typical chain is several adapters followed by exactly one consumer at the end.
What is the difference between iter, iter_mut, and into_iter?
They differ in how they yield elements. iter() yields shared references (&T), letting you read elements without taking ownership. iter_mut() yields mutable references (&mut T), letting you modify elements in place. into_iter() consumes the collection and yields owned values (T), which is useful when you no longer need the original. Choosing the right one depends on whether you want to read, modify, or take ownership of the elements as you iterate.
Why use iterators instead of a plain for loop with an index?
Iterators are more concise, more expressive, and safer. They eliminate manual index bookkeeping, so you cannot go out of bounds or make off-by-one mistakes. Chains of adapters read almost like a description of the transformation you want. And thanks to Rust's zero-cost abstractions, idiomatic iterator chains compile down to code that is just as fast as a hand-written loop — often the compiler produces identical machine code. For these reasons, iterators are the idiomatic way to process sequences in Rust.