Guide contents

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 Iterator trait, 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.

Rust
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());
}
Output
Some("build")
Some("test")
Some("deploy")
None

The trait is short. Everything else is a default method built on next:

Rust
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:

Rust
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!();
}
Output
checkout compile package
checkout compile package

3. 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)
Rust
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()
}
Output
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.

Rust
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);
}
Output
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.

Rust
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);
}
Output
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.

Rust
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");
}
Output
Some(529)
true after checking 3 items

1.. is a range with no end: an infinite iterator.

7. Consuming methods: sum, count, min, max

Rust
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}");
}
Output
total=710 count=5 slowest=Some(300) fastest=Some(85)
10! = 3628800

sum 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.

Rust
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);
}
Output
["ANA", "BEN", "CY"] [3, 3, 2] [2, 4, 6, 8, 10]

9. collect() into a String

Characters and strings collect into a String directly.

Rust
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}");
}
Output
olleh GBH trtrs r lzy

10. collect() into a HashMap or HashSet

An iterator of (key, value) tuples collects into a map. HashMap and HashSet cover these types.

Rust
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);
}
Output
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.

Rust
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);
}
Output
Ok([1, 2, 3])
Err(ParseIntError { kind: PosOverflow })

12. Ranges are iterators

Rust
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);
}
Output
[1, 4, 9, 16, 25] [4, 3, 2, 1, 0] [0, 5, 10, 15, 20, 25, 30] abcdef

13. Strings produce iterators

chars, bytes, lines, split, and split_whitespace all return iterators, which is why text processing chains so well.

Rust
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}");
}
Output
9 words, longest line 'the quick brown fox', initials TQBFJOTLD

14. 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.

Rust
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}");
}
Output
total 19, then None
again 19

by_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.

Rust✗ does not compile · E0382
fn main() {
    let it = vec![1, 2, 3].into_iter();
    let sum: i32 = it.sum();
    let count = it.count();
    println!("{sum} {count}");
}
Output
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.

Rust
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);
}
Output
[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.

Rust
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);
}
Output
[3, 1, 2] ["ana", "ben"] 3

18. enumerate(): items with their index

Rust
fn main() {
    let podium = ["Chen", "Ada", "Bo"];
    for (i, name) in podium.iter().enumerate() {
        println!("{}. {name}", i + 1);
    }
}
Output
1. Chen
2. Ada
3. Bo

19. Chaining several adapters

The typical shape of iterator code: a source, some adapters, and one consumer.

Rust
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);
}
Output
["ana: 120.00", "cy: 310.00"]

20. find and position

Rust
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);
}
Output
Some(("u2", "ben@corp.com")) Some(2) None

21. any and all

Rust
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));
}
Output
all healthy: false, any down: true ["search"]
empty all: true

all 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.

Rust
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());
}
Output
[disk full]
[timeout on db]
2 errors

23. Accepting any iterable: impl IntoIterator

A function taking impl IntoIterator<Item = T> accepts a Vec, an array, a range, or another iterator.

Rust
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())));
}
Output
75
45
10
30

24. 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.

Rust
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));
}
Output
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

Rust
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);
}
Output
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.

Rust
#[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}");
}
Output
[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.0

27. Realistic example: top words in a text

Rust
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}");
    }
}
Output
iterators  3
are        2
lazy       2
rust       2

for_each runs a closure for every item, like a for loop at the end of a chain.

Summary

  • An iterator implements Iterator with next() -> Option<Item>. Every for loop uses one.
  • iter() yields &T, iter_mut() yields &mut T, and into_iter() yields T and 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 a Vec, String, HashMap, HashSet, Result<Vec<_>, _>, and more.
  • Return impl Iterator<Item = T> to hand out lazy pipelines, and accept impl IntoIterator<Item = T> to take any iterable.
  • Iterator pipelines compile to plain loops: no boxing, no overhead.

Next: Iterator Adapters: The Complete Cookbook.