Lesson 38 of 94
Iterators
Rust's iterators are lazy, chainable pipelines of map, filter, and friends. They are also the foundation of every for loop. The key points:
- An iterator is just a value implementing the
Iteratortrait, with one required method,next(). - Pipelines compile to plain loops. There is no boxing, no lambda objects, and no stream setup cost. Rust calls this a zero-cost abstraction.
- Ownership decides what an iterator yields: shared references, mutable references, or owned values.
| Operation | Rust iterator |
|---|---|
| Start from a collection | v.iter() |
| Transform each item | .map(|x| ...) |
| Keep matching items | .filter(|x| ...) |
| Collect into a list | .collect::<Vec<_>>() |
| Count items | .count() |
| Sum items | .sum() |
| First match | .next() or .find(...) |
| Any / all match | .any(...) / .all(...) |
| Range of integers | 0..n |
| Consumption | An iterator is consumed as you call next() |
The next lesson is a cookbook of every adapter. This one covers how iterators work.
1. The Iterator trait and next()
An iterator produces a sequence of values. Each call to next() returns Some(value), or None when the sequence is finished.
fn main() {
let tasks = vec!["build", "test", "deploy"];
let mut it = tasks.iter();
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
}Some("build")
Some("test")
Some("deploy")
NoneThe trait is short. Everything else is a default method built on next:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...over 70 provided methods: map, filter, sum, collect, ...
}2. A for loop is a while let over next()
for x in collection calls into_iter() on the collection, then next() until it returns None. These two loops are the same:
fn main() {
let steps = vec!["checkout", "compile", "package"];
for step in &steps {
print!("{step} ");
}
println!();
let mut it = (&steps).into_iter();
while let Some(step) = it.next() {
print!("{step} ");
}
println!();
}checkout compile package
checkout compile package3. iter(), iter_mut(), and into_iter()
Collections offer three iterators, one for each way of accessing elements. This is the ownership story from the earlier lessons, applied to iteration.
| Method | Yields | for loop form |
Collection afterwards |
|---|---|---|---|
iter() |
&T |
for x in &v |
unchanged |
iter_mut() |
&mut T |
for x in &mut v |
possibly modified |
into_iter() |
T |
for x in v |
consumed (moved) |
fn main() {
let mut prices = vec![10.0, 20.0, 30.0];
let total: f64 = prices.iter().sum(); // &f64 items
println!("total {total}");
for p in prices.iter_mut() { // &mut f64 items
*p *= 1.1;
}
println!("{:?}", prices);
let labels: Vec<String> = prices.into_iter().map(|p| format!("${p:.2}")).collect(); // f64 items
println!("{:?}", labels);
// println!("{:?}", prices); // error: `prices` was moved by into_iter()
}total 60
[11.0, 22.0, 33.0]
["$11.00", "$22.00", "$33.00"]4. Iterators are lazy
Adapters such as map and filter do nothing on their own: they build a pipeline that runs only when something consumes it. The compiler warns about unused lazy iterators.
fn main() {
let numbers = vec![1, 2, 3];
let pipeline = numbers.iter().map(|n| {
println!(" mapping {n}");
n * 10
});
println!("pipeline built, nothing mapped yet");
let result: Vec<i32> = pipeline.collect();
println!("{:?}", result);
}pipeline built, nothing mapped yet
mapping 1
mapping 2
mapping 3
[10, 20, 30]5. Items flow one at a time through the whole pipeline
Each item passes through every stage before the next item starts. Nothing builds an intermediate list.
fn main() {
let result: Vec<i32> = [1, 2, 3, 4]
.iter()
.map(|&n| {
println!("map {n}");
n * n
})
.filter(|&sq| {
println!(" filter {sq}");
sq % 2 == 0
})
.collect();
println!("{:?}", result);
}map 1
filter 1
map 2
filter 4
map 3
filter 9
map 4
filter 16
[4, 16]6. Short-circuiting: work stops as soon as possible
Because iteration is lazy, methods like find, any, and take stop pulling items once they have their answer, even from an infinite sequence.
fn main() {
let first_big_square = (1..).map(|n| n * n).find(|&sq| sq > 500);
println!("{:?}", first_big_square);
let mut checked = 0;
let has_negative = [5, 3, -1, 8, 9].iter().any(|&x| {
checked += 1;
x < 0
});
println!("{has_negative} after checking {checked} items");
}Some(529)
true after checking 3 items1.. is a range with no end: an infinite iterator.
7. Consuming methods: sum, count, min, max
fn main() {
let response_ms = [120, 85, 300, 95, 110];
let total: u32 = response_ms.iter().sum();
let count = response_ms.iter().count();
let slowest = response_ms.iter().max();
let fastest = response_ms.iter().min();
let product: u64 = (1..=10).product();
println!("total={total} count={count} slowest={:?} fastest={:?}", slowest, fastest);
println!("10! = {product}");
}total=710 count=5 slowest=Some(300) fastest=Some(85)
10! = 3628800sum and product need to know the result type, given here by the annotations u32 and u64.
8. collect() into a Vec
collect builds a collection from an iterator. You say which collection with a type annotation or a turbofish.
fn main() {
let names = ["ana", "ben", "cy"];
let upper: Vec<String> = names.iter().map(|n| n.to_uppercase()).collect();
let lengths = names.iter().map(|n| n.len()).collect::<Vec<usize>>();
let evens: Vec<_> = (1..=10).filter(|n| n % 2 == 0).collect();
println!("{:?} {:?} {:?}", upper, lengths, evens);
}["ANA", "BEN", "CY"] [3, 3, 2] [2, 4, 6, 8, 10]9. collect() into a String
Characters and strings collect into a String directly.
fn main() {
let word = "hello";
let reversed: String = word.chars().rev().collect();
let initials: String = ["Grace", "Brewster", "Hopper"].iter().filter_map(|w| w.chars().next()).collect();
let no_vowels: String = "iterators are lazy".chars().filter(|c| !"aeiou".contains(*c)).collect();
println!("{reversed} {initials} {no_vowels}");
}olleh GBH trtrs r lzy10. collect() into a HashMap or HashSet
An iterator of (key, value) tuples collects into a map. HashMap and HashSet cover these types.
use std::collections::{HashMap, HashSet};
fn main() {
let prices: HashMap<&str, u32> = [("apple", 120), ("pear", 95)].into_iter().collect();
println!("pear costs {}", prices["pear"]);
let unique_tags: HashSet<&str> = ["rust", "cli", "rust", "async", "cli"].into_iter().collect();
let mut sorted: Vec<_> = unique_tags.into_iter().collect();
sorted.sort();
println!("{:?}", sorted);
}pear costs 95
["async", "cli", "rust"]11. collect() into Result or Option
Collecting an iterator of Results into a Result<Vec<_>, _> stops at the first error, as you saw in Result.
fn main() {
let good: Result<Vec<u8>, _> = ["1", "2", "3"].iter().map(|s| s.parse::<u8>()).collect();
let bad: Result<Vec<u8>, _> = ["1", "999", "3"].iter().map(|s| s.parse::<u8>()).collect();
println!("{:?}\n{:?}", good, bad);
}Ok([1, 2, 3])
Err(ParseIntError { kind: PosOverflow })12. Ranges are iterators
fn main() {
let squares: Vec<u32> = (1..=5).map(|n| n * n).collect();
let countdown: Vec<i32> = (0..5).rev().collect();
let every_fifth: Vec<u32> = (0..=30).step_by(5).collect();
let letters: String = ('a'..='f').collect();
println!("{:?} {:?} {:?} {letters}", squares, countdown, every_fifth);
}[1, 4, 9, 16, 25] [4, 3, 2, 1, 0] [0, 5, 10, 15, 20, 25, 30] abcdef13. Strings produce iterators
chars, bytes, lines, split, and split_whitespace all return iterators, which is why text processing chains so well.
fn main() {
let text = "the quick brown fox\njumps over\nthe lazy dog";
let words = text.split_whitespace().count();
let longest_line = text.lines().max_by_key(|l| l.len()).unwrap_or("");
let capitals: String = text.split_whitespace().map(|w| w.chars().next().unwrap().to_ascii_uppercase()).collect();
println!("{words} words, longest line '{longest_line}', initials {capitals}");
}9 words, longest line 'the quick brown fox', initials TQBFJOTLD14. An iterator can only be used once
Iterating consumes the iterator's position. Once it returns None, it stays empty. To iterate again, create a new iterator from the collection, which is cheap.
fn main() {
let scores = vec![3, 7, 9];
let mut it = scores.iter();
let total: i32 = it.by_ref().sum();
println!("total {total}, then {:?}", it.next());
let fresh_total: i32 = scores.iter().sum(); // a new iterator from the same Vec
println!("again {fresh_total}");
}total 19, then None
again 19by_ref() lets a consuming method use the iterator without moving it, so it can still be called afterwards.
15. Using an iterator after moving it is an error
Consuming methods take the iterator by value.
fn main() {
let it = vec![1, 2, 3].into_iter();
let sum: i32 = it.sum();
let count = it.count();
println!("{sum} {count}");
}error[E0382]: use of moved value: `it`
--> src/main.rs:4:17
|
2 | let it = vec![1, 2, 3].into_iter();
| -- move occurs because `it` has type `std::vec::IntoIter<i32>`, which does not implement the `Copy` trait
3 | let sum: i32 = it.sum();
| ----- `it` moved due to this method call
4 | let count = it.count();
| ^^ value used here after move
|
note: `std::iter::Iterator::sum` takes ownership of the receiver `self`, which moves `it`
--> /rustc/<version>/library/core/src/iter/traits/iterator.rs:3594:14
help: you can `clone` the value and consume it, but this might not be your desired behavior
|
3 | let sum: i32 = it.clone().sum();
| ++++++++16. into_iter() on arrays and ranges
Arrays and ranges can be consumed by value too. For an array of Copy values, into_iter() yields the values themselves instead of references.
fn main() {
let arr = [1, 2, 3];
let doubled: Vec<i32> = arr.into_iter().map(|x| x * 2).collect(); // x: i32
let from_refs: Vec<i32> = arr.iter().map(|x| x * 2).collect(); // x: &i32 (auto-deref in `*`)
println!("{:?} {:?} {:?}", doubled, from_refs, arr);
}[2, 4, 6] [2, 4, 6] [1, 2, 3]17. copied() and cloned()
iter() yields references. copied() turns &T into T for Copy types, and cloned() clones each item for Clone types.
fn main() {
let ids = vec![3, 1, 2];
let owned_ids: Vec<u32> = ids.iter().copied().collect();
let names = vec![String::from("ana"), String::from("ben")];
let owned_names: Vec<String> = names.iter().cloned().collect();
let max_id: u32 = ids.iter().copied().max().unwrap_or(0);
println!("{:?} {:?} {max_id}", owned_ids, owned_names);
}[3, 1, 2] ["ana", "ben"] 318. enumerate(): items with their index
fn main() {
let podium = ["Chen", "Ada", "Bo"];
for (i, name) in podium.iter().enumerate() {
println!("{}. {name}", i + 1);
}
}1. Chen
2. Ada
3. Bo19. Chaining several adapters
The typical shape of iterator code: a source, some adapters, and one consumer.
fn main() {
let orders = [("ana", 120.0, true), ("ben", 35.5, false), ("cy", 310.0, true), ("dee", 89.9, true)];
let paid_over_100: Vec<String> = orders
.iter()
.filter(|(_, _, paid)| *paid)
.filter(|(_, total, _)| *total > 100.0)
.map(|(name, total, _)| format!("{name}: {total:.2}"))
.collect();
println!("{:?}", paid_over_100);
}["ana: 120.00", "cy: 310.00"]20. find and position
fn main() {
let users = [("u1", "ana@example.com"), ("u2", "ben@corp.com"), ("u3", "cy@corp.com")];
let first_corp = users.iter().find(|(_, email)| email.ends_with("@corp.com"));
let index = users.iter().position(|(id, _)| *id == "u3");
let missing = users.iter().find(|(id, _)| *id == "u9");
println!("{:?} {:?} {:?}", first_corp, index, missing);
}Some(("u2", "ben@corp.com")) Some(2) None21. any and all
fn main() {
let health_checks = [("db", true), ("cache", true), ("search", false)];
let all_healthy = health_checks.iter().all(|(_, ok)| *ok);
let any_down = health_checks.iter().any(|(_, ok)| !ok);
let down: Vec<&str> = health_checks.iter().filter(|(_, ok)| !ok).map(|(name, _)| *name).collect();
println!("all healthy: {all_healthy}, any down: {any_down} {:?}", down);
println!("empty all: {}", Vec::<bool>::new().iter().all(|b| *b));
}all healthy: false, any down: true ["search"]
empty all: trueall on an empty iterator is true, and any is false.
22. Returning iterators from functions
Returning impl Iterator<Item = T> hands the caller a lazy pipeline without building a collection.
fn error_lines(log: &str) -> impl Iterator<Item = &str> {
log.lines().filter(|l| l.starts_with("ERROR")).map(|l| l.trim_start_matches("ERROR ").trim())
}
fn main() {
let log = "INFO start\nERROR disk full\nWARN slow\nERROR timeout on db ";
for e in error_lines(log) {
println!("[{e}]");
}
println!("{} errors", error_lines(log).count());
}[disk full]
[timeout on db]
2 errors23. Accepting any iterable: impl IntoIterator
A function taking impl IntoIterator<Item = T> accepts a Vec, an array, a range, or another iterator.
fn total_minutes(durations: impl IntoIterator<Item = u32>) -> u32 {
durations.into_iter().sum()
}
fn main() {
println!("{}", total_minutes(vec![30, 45]));
println!("{}", total_minutes([15, 15, 15]));
println!("{}", total_minutes(1..=4));
println!("{}", total_minutes(["10", "x", "20"].iter().filter_map(|s| s.parse().ok())));
}75
45
10
3024. Iterators are as fast as hand-written loops
The compiler turns an iterator pipeline into the same machine code as a manual loop, and bounds checks often disappear because the iterator can't go out of range. Prefer iterators over index loops; they are clearer and at least as fast.
fn sum_of_even_squares_loop(v: &[u64]) -> u64 {
let mut total = 0;
for i in 0..v.len() {
if v[i] % 2 == 0 {
total += v[i] * v[i];
}
}
total
}
fn sum_of_even_squares_iter(v: &[u64]) -> u64 {
v.iter().filter(|&&x| x % 2 == 0).map(|&x| x * x).sum()
}
fn main() {
let data: Vec<u64> = (1..=1_000).collect();
println!("{} {}", sum_of_even_squares_loop(&data), sum_of_even_squares_iter(&data));
}167167000 167167000|&&x| destructures a &&u64 (filter's closure receives a reference to the item, and the item is itself a &u64).
25. Realistic example: a sales summary
struct Sale {
region: &'static str,
product: &'static str,
units: u32,
unit_price_cents: u64,
}
fn main() {
let sales = vec![
Sale { region: "north", product: "widget", units: 10, unit_price_cents: 250 },
Sale { region: "south", product: "gadget", units: 3, unit_price_cents: 1_999 },
Sale { region: "north", product: "gadget", units: 1, unit_price_cents: 1_999 },
Sale { region: "east", product: "widget", units: 25, unit_price_cents: 240 },
];
let revenue: u64 = sales.iter().map(|s| s.units as u64 * s.unit_price_cents).sum();
let widget_units: u32 = sales.iter().filter(|s| s.product == "widget").map(|s| s.units).sum();
let best = sales.iter().max_by_key(|s| s.units as u64 * s.unit_price_cents).unwrap();
let regions: Vec<&str> = {
let mut r: Vec<&str> = sales.iter().map(|s| s.region).collect();
r.sort();
r.dedup();
r
};
println!("revenue {:.2}, widgets sold {widget_units}", revenue as f64 / 100.0);
println!("best sale: {} {} in {}", best.units, best.product, best.region);
println!("regions: {:?}", regions);
}revenue 164.96, widgets sold 35
best sale: 25 widget in east
regions: ["east", "north", "south"]26. Realistic example: parsing and validating a CSV file
Each line goes through a pipeline: skip the header, skip blanks, parse, and keep the errors with their line numbers.
#[derive(Debug)]
struct Row {
name: String,
age: u8,
}
fn parse_row(line: &str) -> Result<Row, String> {
let (name, age) = line.split_once(',').ok_or("missing comma")?;
let age = age.trim().parse::<u8>().map_err(|e| format!("bad age '{}': {e}", age.trim()))?;
Ok(Row { name: name.trim().to_string(), age })
}
fn main() {
let csv = "name,age\nAda,36\n\nGrace,abc\nLinus,54\nno comma here";
let (rows, errors): (Vec<_>, Vec<_>) = csv
.lines()
.enumerate()
.skip(1)
.filter(|(_, line)| !line.trim().is_empty())
.map(|(i, line)| parse_row(line).map_err(|e| format!("line {}: {e}", i + 1)))
.partition(|r| r.is_ok());
let rows: Vec<Row> = rows.into_iter().map(Result::unwrap).collect();
let errors: Vec<String> = errors.into_iter().map(Result::unwrap_err).collect();
println!("{:?}", rows);
println!("{:?}", errors);
let average_age = rows.iter().map(|r| r.age as f64).sum::<f64>() / rows.len() as f64;
println!("average age {average_age:.1}");
}[Row { name: "Ada", age: 36 }, Row { name: "Linus", age: 54 }]
["line 4: bad age 'abc': invalid digit found in string", "line 6: missing comma"]
average age 45.027. Realistic example: top words in a text
use std::collections::HashMap;
fn main() {
let text = "Rust iterators are lazy. Rust iterators are fast. Lazy iterators compose!";
let mut counts: HashMap<String, usize> = HashMap::new();
text.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
.map(|w| w.to_lowercase())
.for_each(|w| *counts.entry(w).or_insert(0) += 1);
let mut ranked: Vec<(String, usize)> = counts.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
for (word, n) in ranked.iter().take(4) {
println!("{word:<10} {n}");
}
}iterators 3
are 2
lazy 2
rust 2for_each runs a closure for every item, like a for loop at the end of a chain.
Summary
- An iterator implements
Iteratorwithnext() -> Option<Item>. Everyforloop uses one. iter()yields&T,iter_mut()yields&mut T, andinto_iter()yieldsTand consumes the collection.- Adapters (
map,filter, ...) are lazy. Consumers (collect,sum,count,find, ...) run the pipeline, one item at a time, stopping early when they can. collect()builds aVec,String,HashMap,HashSet,Result<Vec<_>, _>, and more.- Return
impl Iterator<Item = T>to hand out lazy pipelines, and acceptimpl IntoIterator<Item = T>to take any iterable. - Iterator pipelines compile to plain loops: no boxing, no overhead.
