title: Iterators

An iterator is anything that produces a sequence of values one at a time. Rust’s iterator system lets you process collections in a expressive, chainable way without writing manual loops.

Getting an iterator

let v = vec![1, 2, 3, 4, 5];
 
let iter = v.iter();       // yields &i32 (borrows elements)
let iter = v.iter_mut();   // yields &mut i32 (mutable borrows)
let iter = v.into_iter();  // yields i32 (consumes the vec, takes ownership)

Most of the time you’ll use iter() for read-only access.

for loops use iterators under the hood

let v = vec![1, 2, 3];
 
for n in &v {
    println!("{n}");
}

This is syntactic sugar — Rust calls .into_iter() on &v behind the scenes.

Iterator adapters

Adapters transform an iterator into a new iterator. They are lazy — nothing runs until you consume the iterator.

map — transform each element

let doubled: Vec<i32> = vec![1, 2, 3]
    .iter()
    .map(|n| n * 2)
    .collect();
 
// [2, 4, 6]

filter — keep elements matching a condition

let evens: Vec<&i32> = vec![1, 2, 3, 4, 5]
    .iter()
    .filter(|n| *n % 2 == 0)
    .collect();
 
// [2, 4]

chain adapters together

let result: Vec<i32> = vec![1, 2, 3, 4, 5]
    .iter()
    .filter(|n| *n % 2 != 0)  // keep odds
    .map(|n| n * 10)           // multiply by 10
    .collect();
 
// [10, 30, 50]

Consuming the iterator

Adapters are lazy — you need a consumer to actually run the chain and produce a result.

collect — gather into a collection

let v: Vec<i32> = (1..=5).collect();  // [1, 2, 3, 4, 5]

sum and product

let total: i32 = vec![1, 2, 3, 4, 5].iter().sum();  // 15

count

let n = vec![1, 2, 3].iter().count();  // 3

any and all

let has_even = vec![1, 2, 3].iter().any(|n| n % 2 == 0);  // true
let all_positive = vec![1, 2, 3].iter().all(|n| *n > 0);  // true

find — first matching element

let first_even = vec![1, 2, 3, 4].iter().find(|n| *n % 2 == 0);
// Some(2)

Returns an OptionNone if nothing matches.

Ranges as iterators

Ranges are iterators too:

for i in 0..5 {
    println!("{i}");  // 0, 1, 2, 3, 4
}
 
let squares: Vec<i32> = (1..=5).map(|n| n * n).collect();
// [1, 4, 9, 16, 25]

Lazy vs eager

Iterator adapters (.map(), .filter(), .take(), etc.) are lazy — they return a new iterator type and do no work yet. You can keep chaining off them.

Iterator consumers (.collect(), .sum(), .count(), .for_each(), etc.) are eager — they drive the chain to completion and produce a concrete result. You can’t chain further off them.

A quick test: if you can keep chaining, it’s lazy. If it gives you a final value, it’s eager.

vec![1, 2, 3]
    .iter()          // lazy — returns Iter<i32>
    .map(|n| n * 2)  // lazy — returns Map<...>
    .filter(|n| n > 2) // lazy — returns Filter<...>
    .collect::<Vec<_>>() // eager — drives the chain, returns Vec<i32>

Why laziness matters

Without laziness, each step would allocate an intermediate vector:

// hypothetical eager approach — 3 allocations
let mapped: Vec<i32> = input.map(...);   // allocates 1M elements
let filtered: Vec<i32> = mapped.filter(...); // allocates again
let result = filtered.collect();         // allocates again

With lazy chaining, each element flows through the entire chain one at a time — only one allocation at the end:

// actual Rust — 1 allocation
let result: Vec<i32> = input.iter().map(...).filter(...).collect();

This also means a chain like this never processes any elements at all:

let _ = vec![1, 2, 3].iter().map(|n| n * 2);  // nothing happens, no consumer

And early-terminating consumers stop the chain as soon as they have what they need:

let first = (0..).find(|n| n * n > 100);  // stops at 11, never iterates further