The Iterator Model in Rust
Every collection in Rust's standard library - Vec, HashMap, arrays, slices - exposes its elements through the same small interface: a type that knows how to produce "the next item" until it runs out. That interface, the Iterator trait, is the thread that ties together for loops, map/filter chains, and most of the functional-flavored code you will write in Rust. Understanding it as a model, rather than as a bag of methods to memorize, makes the rest of this section's pages - the trait itself, adapters, consumption, and IntoIterator - click into place as variations on one idea.
This page is the conceptual anchor for the section. It does not walk through collection APIs the way Collections Basics does; instead it explains why iterators are lazy, how ownership decides which of iter, iter_mut, or into_iter you reach for, and what the compiler is actually doing when it turns a chain of adapters into machine code.
Summary
- An iterator is any type implementing
Iterator, which reduces to one method,next, that returnsSome(item)orNone; everything else in the ecosystem is built on top of that single operation. - Insight: Rust needs one shared vocabulary for "a sequence of values I can walk through," and iterators let collections, generators of computed values, and I/O readers all speak it without allocating extra buffers or sacrificing performance.
- Key Concepts: laziness, adapters vs consumers, ownership modes (
iter,iter_mut,into_iter), zero-cost abstraction,IntoIterator. - When to Use: Transforming or filtering a sequence without hand-rolled indices; writing generic code over "anything iterable"; building custom sequence types; choosing whether a loop should borrow, mutate, or consume its source.
- Limitations/Trade-offs: Chains are single-pass and consume themselves; some patterns (windowed lookback, index-and-neighbor access) are genuinely more awkward with adapters than with a plain indexed loop.
- Related Topics: closures and
Fntraits, ownership and borrowing, generics and trait bounds, slices.
Foundations
At its core, Iterator is a trait with one required method:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Everything else - map, filter, take, sum, collect - is a default method built on repeated calls to next. A type becomes an iterator by implementing this one method; the rest of the trait's roughly seventy methods come for free.
The most important property of this model is laziness. Writing (0..10).filter(|n| n % 2 == 0).map(|n| n * 2) does not loop over ten numbers. It builds a small nested struct - a Filter wrapping a Map wrapping a Range - and does no work at all. Nothing happens until you call something that repeatedly asks for next, such as a for loop, .collect(), or .sum(). Rust's compiler even attaches #[must_use] to iterator adapters specifically so an unused chain produces a warning instead of silently doing nothing.
It helps to separate the vocabulary into two families. Adapters (map, filter, zip, enumerate, take, skip, flat_map) wrap an iterator in another iterator, transforming what next will eventually produce. Consumers (collect, sum, fold, count, for_each, and the implicit consumption inside a for loop) are the methods that actually call next in a loop and produce a final, non-iterator value. A chain of adapters with no consumer at the end is inert - it is a recipe, not a result.
Mechanics & Interactions
The three ways to turn a collection into an iterator - iter(), iter_mut(), into_iter() - are not three unrelated methods to memorize. They are the same underlying idea, "give me an iterator over this collection's elements," expressed through Rust's three ownership modes.
collection.iter() -> Iterator<Item = &T> (shared borrow)
collection.iter_mut() -> Iterator<Item = &mut T> (exclusive borrow)
collection.into_iter() -> Iterator<Item = T> (owned, consumes collection)
iter() walks the collection by shared reference, leaving it usable afterward. iter_mut() walks it by exclusive reference, letting you mutate elements in place - and, as with any &mut borrow, the borrow checker guarantees no other code can alias the collection while you do. into_iter() moves the collection itself into the iterator, handing out owned values and leaving the original binding unusable. A plain for x in some_vec desugars to some_vec.into_iter(), which is exactly why iterating a Vec by value inside a for loop moves it; writing for x in &some_vec instead calls the IntoIterator implementation on &Vec<T>, which internally defers to iter().
This is the IntoIterator trait doing its job: it is the trait that makes for loops work at all, and any type that implements it can be dropped into a for loop or accepted by a generic function bounded with impl IntoIterator<Item = T>. Iterator itself implements IntoIterator (returning itself), which is why a for loop happily accepts either a fresh iterator or a collection.
The phrase "zero-cost abstraction," applied to iterator chains, has a specific and checkable meaning: it does not mean adapters are free to reason about, only that the compiled output does not pay a runtime tax for using them. Each map/filter/take in a chain is a distinct generic struct, monomorphized per closure type at compile time. LLVM then inlines the whole nested structure and, in most cases, fuses it into a loop indistinguishable from one you would have written by hand with indices. This is a real, measurable property, not marketing language - but it depends on the compiler being able to see through the whole chain, which mostly holds for straight-line code and can break down across dynamic dispatch (Box<dyn Iterator>) or unpredictable branches.
// Nested adapter types are visible if you try to name them:
fn evens_doubled(v: &[i32]) -> std::iter::Map<std::iter::Filter<std::slice::Iter<i32>, fn(&&i32) -> bool>, fn(&i32) -> i32> {
// Real code uses `impl Iterator<Item = i32>` here instead - the concrete
// type is unnameable in practice, which is *why* `impl Trait` exists.
unimplemented!()
}The snippet above is deliberately impractical: it shows that a two-step chain already has a type too unwieldy to write out, which is precisely the pressure that led to impl Trait return types for functions that produce iterators.
Advanced Considerations & Applications
Because chains are lazy and single-pass, a few structural decisions recur constantly once code gets more complex.
Ordering adapters matters for both correctness and cost: filter before map skips transformation work for discarded elements, while map before filter does the transform unconditionally. filter_map collapses a filter-then-map (or map-then-filter-on-Option) into one pass, avoiding an intermediate allocation that filter().map() does not actually need but that a naive collect()-then-iter() pipeline would introduce.
Consuming an iterator is permanent. Once next has returned None (or once you've handed the iterator to collect), the value is exhausted; there is no "rewind." Code that needs the same data walked twice must either collect() into an owned collection first and iterate that twice, or restructure to avoid the second pass - there is no cost-free way to "replay" a lazy chain.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Iterator adapter chain | Reads as the transformation, compiles to a tight loop, composes | Awkward for neighbor/lookback access, single-pass only | Linear transform/filter/aggregate pipelines |
Manual indexed for loop | Full control, easy neighbor/windowed access | More bookkeeping, easier to introduce off-by-one bugs | Algorithms that need position or adjacent-element context |
rayon's parallel iterators | Same adapter vocabulary, spreads CPU-bound work across threads | Overhead not worth it for small or I/O-bound sequences | Large, CPU-heavy, embarrassingly parallel transforms |
In async code, the iterator model has a sibling rather than an extension: Stream (from the futures ecosystem, and increasingly tokio) mirrors Iterator's laziness and adapter vocabulary but yields items across .await points instead of synchronously. The two are conceptually the same shape - "pull the next item on demand" - but they are different traits with different consumers, so iterator adapters and stream adapters do not mix without an explicit conversion.
Custom iterators are also how Rust models "infinite" or generated sequences safely: a Counter struct implementing Iterator with no natural end is completely valid, because laziness means nothing runs until something bounds it. That is also the trap - collect()-ing an unbounded iterator without a take(n) first will hang or exhaust memory, since collect has no way to know the sequence is unbounded until it stops receiving None.
Common Misconceptions
- "Iterators are just a nicer syntax for loops" - The syntax is nicer, but the deeper point is that a chain is a value (an inert struct) before it is a computation; nothing runs until a consumer drives it, which is easy to miss coming from languages where list operations execute eagerly.
- "
iter()andinto_iter()are interchangeable" - They differ in what they hand you (&TvsT) and whether the source collection survives afterward; picking the wrong one is the single most common iterator-related compile error for newcomers. - "Zero-cost means writing the chain has no cost" - It means the compiled program pays no extra runtime cost versus a hand-written loop; reading and reasoning about a five-adapter chain still has real cognitive cost, and heavy dynamic dispatch (
Box<dyn Iterator>) can reintroduce runtime overhead. - "
map/filterallocate an intermediate collection at each step" - They don't; each adapter only wraps the previous iterator in a struct, and no buffer exists until a consumer likecollectactually allocates one. - "An iterator chain can be iterated more than once" - Once consumed, an iterator is exhausted; reusing the same chain for a second pass silently produces an iterator that immediately returns
None, not a fresh run.
FAQs
What is the minimum a type needs to implement to become an iterator?
Just type Item and fn next(&mut self) -> Option<Self::Item>. Every other method (map, filter, sum, collect, and dozens more) is a default method derived from next, so implementing one function unlocks the whole adapter vocabulary.
Why doesn't `let x = v.iter().map(|n| n * 2);` print anything or do any work?
Because the chain is lazy - map returns a Map struct wrapping the underlying iterator, and no code runs until something calls next on it (directly, via a for loop, or via a consumer like collect or sum). The compiler even warns on unused iterator values via #[must_use].
How do I decide between `iter()`, `iter_mut()`, and `into_iter()`?
Ask what you need to do with each element: read it and keep the collection afterward (iter()), mutate it in place and keep the collection (iter_mut()), or take ownership of each element and give up the collection (into_iter()). The choice mirrors borrowing a value, borrowing it mutably, or moving it - the same three modes that apply everywhere else in Rust.
Why does `for x in some_vec { ... }` make `some_vec` unusable afterward?
A bare for x in collection desugars to collection.into_iter(), which consumes the collection by value. Writing for x in &some_vec instead calls IntoIterator on the reference type, which yields borrowed items and leaves some_vec intact.
What actually connects `for` loops to the `Iterator` trait?
The IntoIterator trait. A for loop is syntax sugar that calls .into_iter() on its right-hand side and then repeatedly calls .next() on the result until it returns None. Any type implementing IntoIterator - not just Iterator itself - can appear after in.
Does chaining five or six adapters actually cost anything at runtime?
In the common case, no measurable extra cost beyond a hand-written loop, because LLVM inlines and fuses the nested adapter structs into straight-line code. This can break down when the compiler loses visibility into the chain - for example behind Box<dyn Iterator> trait objects, which trade the zero-cost property for object-safety and heterogeneous storage.
Why does `.filter(...).map(...)` sometimes get written as `.filter_map(...)` instead?
filter_map fuses a filter-then-transform into a single pass over a single closure returning Option<T>, which is both more direct to read for that specific pattern and avoids reasoning about two separate adapter layers for what is conceptually one decision per element.
Can I iterate over the same `Vec` twice with the same iterator?
No - once an iterator is exhausted (or moved into a consumer like collect), it cannot be reused. To walk the same data twice, either call .iter() again from the original collection, or .collect() once into an owned collection you can iterate repeatedly.
Is it safe to write an iterator that never ends?
Yes, and it's a normal pattern (an infinite counter, a repeating cycle) precisely because laziness means an unbounded iterator does no work until driven. The danger is downstream: calling .collect() or another unbounded consumer on it without first applying .take(n) will hang or exhaust memory, since nothing tells collect in advance that the sequence never stops.
How does an iterator relate to a closure passed into `map` or `filter`?
The closure is stored inside the adapter struct (Map, Filter, etc.) and called from within that adapter's next implementation. This is one reason closures and iterators are covered as a pair in this cookbook - most non-trivial adapter chains are, structurally, an iterator holding a closure.
What is the difference between an iterator adapter and an iterator consumer?
Adapters (map, filter, zip, take) wrap an iterator in another iterator and stay lazy. Consumers (collect, sum, fold, for_each, and the for loop itself) call next in a loop and produce a concrete, non-iterator result, which is what actually triggers all the deferred work in the chain.
Are Rust's async `Stream`s just async iterators?
Conceptually yes - Stream mirrors Iterator's lazy, pull-based, adapter-driven shape - but they are a distinct trait whose items arrive across .await points rather than synchronously, so Iterator adapters and Stream adapters are not directly interchangeable without an explicit bridge.
Why does the compiler warn about an iterator chain that "does nothing"?
Because Iterator's adapter methods are marked #[must_use]; if you build a chain and never consume it (never call a method that drives next), the compiler assumes that's a mistake, since a lazy chain with no consumer has no observable effect at all.
Related
- Collections Basics - the hands-on companion to this page, for choosing among
Vec,HashMap,BTreeMap, and friends. - The Iterator Trait - the mechanics of implementing
Iteratoryourself, includingsize_hintandFusedIterator. - Iterator Adapters - a working tour of
map,filter,zip,flat_map, and chain ordering. - IntoIterator & Owned vs Borrowed - a deeper, recipe-driven look at
iter/iter_mut/into_iter. - Iterator + Closure Patterns - how closures captured inside adapters interact with ownership.
- Fallible Iterators & collect() - collecting into
Result<Vec<T>, E>and short-circuiting on error.
Stack versions: This page was written for Rust 1.97.0 (edition 2024). It is conceptual and not tied to a specific crate version beyond the standard library.