Iterators

Lazy Evaluation

Iterators and iterator adapters in Rust are lazy: constructing an iterator or chaining adapters (.map(), .filter(), .take()) performs no computation and allocates no intermediate collections until a consumer actively pulls elements from the iterator.

Adapter Chains Without a Consumer Do Nothing

Calling .map(...) on an iterator simply returns a lightweight std::iter::Map struct containing the source iterator and the closure. If no consumer (such as .collect(), .sum(), .for_each(), or a for loop) drives the Map struct, the closure inside .map(...) is never executed:

fn main() {
    let numbers = [1, 2, 3];

    // Creating a lazy adapter without consuming it:
    let _unused_pipeline = numbers.iter().map(|x| {
        println!("Mapping {x}"); // Never runs!
        x * 10
    });

    println!("Pipeline created, now consuming a second pipeline:");

    let results: Vec<i32> = numbers
        .iter()
        .map(|x| {
            println!("  Computing {x} * 10");
            x * 10
        })
        .collect();

    println!("Collected: {results:?}");
}
Pipeline created, now consuming a second pipeline:
  Computing 1 * 10
  Computing 2 * 10
  Computing 3 * 10
Collected: [10, 20, 30]

Compiler Warning on Unused Iterator Adapters (#[must_use])

Because iterator adapters have no effect unless consumed, every standard library iterator adapter is annotated with #[must_use]. Dropping an adapter chain without consuming it triggers an unused_must_use compiler warning:

warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     [1, 2, 3].iter().map(|x| println!("{x}"));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default

If you want to run a closure for its side effects on each element, terminate the chain with .for_each(|x| ...) or use a for loop instead of an unconsumed .map(...).

Benefits of Lazy Evaluation

  1. Zero intermediate allocations: Chaining .filter().map().enumerate() processes one item all the way through the pipeline per .next() call rather than allocating temporary Vec buffers between steps.
  2. Short-circuiting stops work early: Pairing an expensive .map() with .take(2) or .find(...) evaluates the closure only for the elements actually needed:
fn main() {
    let first_match = (1..=1_000_000)
        .map(|n| {
            println!("Evaluating n = {n}");
            n * n
        })
        .find(|&sq| sq > 10);

    println!("Found: {first_match:?}");
}
Evaluating n = 1
Evaluating n = 2
Evaluating n = 3
Evaluating n = 4
Found: Some(16)