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
- Zero intermediate allocations: Chaining
.filter().map().enumerate()processes one item all the way through the pipeline per.next()call rather than allocating temporaryVecbuffers between steps. - 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)