Iterators
Understand Rust's iterator system, from the core traits and common adapter methods to building your own iterators and choosing them over explicit loops.
Iterators are Rust's primary mechanism for processing sequences of values — they show up in for loops, collections, functional pipelines, and anywhere you need to step through items one by one. They are designed to be zero-cost abstractions, meaning the high-level, declarative code you write compiles down to the same machine code you would get from a hand-written loop. This document walks through the entire iterator system: the two foundational traits, how to obtain iterators from collections, the adapter methods that transform them, the consuming methods that drive them, and how to build iterators for your own types. The final section compares iterator pipelines with explicit loops so you can decide when each style makes sense.
The Iterator and IntoIterator Traits
Two traits sit at the center of Rust's iteration model: Iterator and IntoIterator. Iterator defines how a sequence produces values; IntoIterator defines how a type can be turned into an iterator. Every collection you iterate over, every for loop, and every method chain traces back to these traits.
Iterator
The Iterator trait is the heart of the entire system. Its minimal definition requires implementing just one function — next — and you get dozens of powerful methods for free.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
type Item is an associated type that names the kind of value the iterator produces. next advances the iterator and returns either Some(item) if there is another element, or None once the sequence is exhausted. After next returns None, calling it again will typically continue returning None — an exhausted iterator is expected to stay exhausted, though this is a convention, not a compiler-enforced rule.
This design exists because it decouples the idea of "a sequence of things" from any specific data structure. A Vec, a range of numbers, a file's lines, and an endlessly repeating value can all present the same interface. Code that operates on an iterator works on all of them without knowing the details.
A good mental model: picture an iterator as a bookmark inside a book. next moves the bookmark to the next page and hands you that page. When the bookmark reaches the back cover, next returns None — there are no more pages. The bookmark doesn't know how many pages there are or what the book looks like; it only knows how to advance.
let values = vec![10, 20, 30];
let mut iter = values.iter();
assert_eq!(iter.next(), Some(&10));
assert_eq!(iter.next(), Some(&20));
assert_eq!(iter.next(), Some(&30));
assert_eq!(iter.next(), None);
This example uses iter(), which borrows the vector. The iterator yields &i32 references, not owned values. The original vector remains untouched and available after the iterator is consumed.
An Exhausted Iterator May Not Reset:
Calling next after None is not a logic error, but many iterators will remain exhausted forever — they will not restart. To iterate a collection again, create a fresh iterator by calling .iter() or .into_iter() a second time.
IntoIterator
Iterator answers "how do I walk through a sequence?" — IntoIterator answers "can this thing be turned into a sequence?" It is the trait that enables for loops.
pub trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
fn into_iter(self) -> Self::IntoIter;
}
When you write for x in some_value, the compiler calls some_value.into_iter() to produce an iterator, then repeatedly calls next on that iterator, binding each value to x, until None terminates the loop. This conversion is the only bridge between a type and a for loop.
The real power comes from the fact that IntoIterator can be implemented separately for an owned type, a shared reference, and a mutable reference. That is why these three patterns behave differently:
let v = vec![1, 2, 3];
// Borrows v — iterates over &i32
for x in &v {
println!("{}", x);
}
// Mutably borrows v — iterates over &mut i32
for x in &mut v {
*x += 1;
}
// Consumes v — iterates over i32, v is gone afterward
for x in v {
println!("{}", x);
}
The compiler picks the correct IntoIterator implementation based on the type of the expression in the for loop. &Vec<i32> produces an iterator over &i32, &mut Vec<i32> produces &mut i32, and Vec<i32> produces owned i32. This is why you don't see iter() or into_iter() inside a for loop — the loop handles the conversion implicitly.
Consuming a Collection Accidentally:
If you use a plain for x in my_vec where my_vec is not borrowed, the vector is moved and cannot be used afterward. This is a common early surprise. Prefer for x in &my_vec unless you genuinely need to take ownership of each element.
Creating Iterators
Beyond the implicit conversion in for loops, you create iterators explicitly through a handful of methods and standard library functions.
The Three Collection Methods
Most standard collections provide three entry points, and the distinctions are entirely about ownership:
| Method | Yields | Collection afterward |
|---|---|---|
.iter() | &T | untouched, available |
.iter_mut() | &mut T | untouched, available (but must be mut) |
.into_iter() | T (owned) | consumed, cannot be used |
let mut data = vec![1, 2, 3];
// Immutable borrow — data still usable
let doubled: Vec<i32> = data.iter().map(|x| x * 2).collect();
println!("Original: {:?}", data); // [1, 2, 3]
println!("Doubled: {:?}", doubled); // [2, 4, 6]
// Mutable borrow — modifies in place
data.iter_mut().for_each(|x| *x *= 10);
println!("After iter_mut: {:?}", data); // [10, 20, 30]
// Ownership transfer — data is moved
let sum: i32 = data.into_iter().sum();
println!("Sum: {}", sum);
// data is no longer accessible here
Choosing which one to use is a decision about what you need: do you only need to look at the elements (iter), modify them in place (iter_mut), or take them and destroy the collection (into_iter)? When in doubt, start with iter — it is the least destructive.
Iterators from Ranges and Built-ins
Rust's range syntax produces iterators directly. 0..5 yields an iterator over the integers 0 through 4; 0..=5 includes 5. Because ranges implement IntoIterator, you can use them in for loops without any conversion.
The Option and Result types also implement IntoIterator. An Option produces either zero or one element — a concise way to conditionally inject a value into a chain.
let maybe = Some(42);
let extracted: Vec<i32> = maybe.into_iter().collect();
assert_eq!(extracted, vec![42]);
let none: Option<i32> = None;
let empty: Vec<i32> = none.into_iter().collect();
assert!(empty.is_empty());
Ad-Hoc Iterator Constructors
The standard library provides standalone functions for building iterators when you don't have a collection:
std::iter::once(value)— yieldsvalueexactly once, then ends.std::iter::repeat(value)— repeatsvalueforever. Call.take(n)to bound it.std::iter::repeat_with(|| { ... })— calls the closure each time to produce the next value, forever.std::iter::from_fn(|| { ... })— a general-purpose factory; the closure returnsOption<T>, and the iterator stops onNone.std::iter::successors(Some(seed), |prev| { ... })— generates a sequence where each value is derived from the previous one.
use std::iter;
let first_ten_evens: Vec<i32> = iter::from_fn(|| {
static mut CURRENT: i32 = -2;
unsafe {
*CURRENT += 2;
if *CURRENT <= 20 { Some(*CURRENT) } else { None }
}
}).collect();
println!("{:?}", first_ten_evens);
from_fn Is Your Escape Hatch:
from_fn is the simplest way to wrap an imperative state machine as an iterator without defining a new type and implementing Iterator yourself. If your sequence feels awkward as a struct, try from_fn first — it often leads to shorter, clearer code.
Iterator Adapters
Adapter methods take an iterator and return a new iterator that transforms, filters, or restructures the sequence. The defining trait of adapters is laziness: calling an adapter does no work; it only wires up a chain of operations that will execute later, when a consuming method is called.
Core Transform Adapters
The most frequently used adapters are map, filter, filter_map, and flat_map.
map(|x| …)— applies the closure to every element, yielding a new iterator of the closure's return type.filter(|x| …)— keeps only elements for which the closure returnstrue.filter_map(|x| …)— the closure returnsOption<U>; elements producingNoneare dropped, andSome(u)values appear in the output. This is equivalent to amapfollowed by afilterthat discardsNones, but in a single pass.flat_map(|x| …)— the closure returns something that implementsIntoIterator. Each returned sub-iterator is flattened into a single continuous sequence. For example, turning a vector of words into an iterator of their characters.
let words = ["hello", "world"];
let chars: Vec<char> = words.iter()
.flat_map(|w| w.chars())
.collect();
println!("{:?}", chars);
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
Structure and Selection Adapters
These methods rearrange which elements appear and in what order:
enumerate()— wraps each element in a tuple(index, element), withindexstarting at 0.take(n)— stops the iterator afternelements.skip(n)— discards the firstnelements.chain(other_iter)— appends another iterator after this one finishes.zip(other_iter)— pairs elements from two iterators into tuples, stopping when either iterator ends.cycle()— repeats the sequence forever. Only works if the underlying iterator implementsClone.cloned()— clones each&Tinto aT. Use when you have an iterator of references but need owned values in the output.copied()— likecloned, but forCopytypes. Slightly more explicit and sometimes more efficient.
Beware of cycle Without a Bound:
cycle() produces an infinite iterator. Using collect() on it will run until memory is exhausted — the compiler won't stop you. Always pair cycle with take or another limiting adapter unless you genuinely want an endless loop.
Laziness in Practice
Because adapters are lazy, a pipeline like v.iter().filter(|x| x > &5).map(|x| x * 2) does not touch any element until a consumer is invoked. You can store such a pipeline in a variable and use it later without incurring the cost prematurely.
let numbers = vec![1, 3, 7, 9, 2];
let pipeline = numbers.iter().filter(|x| *x > &5).map(|x| x * 2);
// Nothing has been computed yet.
let result: Vec<i32> = pipeline.collect();
// Now the filtering and mapping execute, on demand.
println!("{:?}", result); // [14, 18]
The mental model: adapter chains are recipes, not actions. The consuming method is the cook who finally reads the recipe and does the work.
Consuming Iterators
Consuming methods are the endpoints that drive the iterator. They actually call next repeatedly, drain the sequence, and produce a final value or side effect.
Collecting into a Container
collect() is the most general consumer. It gathers the iterator's items into any type that implements FromIterator. The destination type must usually be specified with the turbofish syntax or type annotation.
let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
let as_hash_set: std::collections::HashSet<i32> = (1..=5).collect();
let as_string: String = "hello".chars().collect();
collect can also build more exotic types like Result<Vec<T>, E> from an iterator of Result<T, E> — it short-circuits on the first Err.
Aggregation Consumers
When you need a single value computed from the entire sequence, fold, reduce, and their specialized cousins are the tools:
fold(initial, |accumulator, element| { ... })— walks through all items, updating an accumulator. It starts with the providedinitialvalue.reduce(|accumulator, element| { ... })— likefold, but uses the first element as the initial accumulator. ReturnsNoneon an empty iterator.sum(),product()— sum or multiply all elements (for numeric types that implementSum/Product).min(),max()— find the smallest or largest element, returningNoneif the iterator is empty.
let numbers = [4, 7, 1, 3];
let sum: i32 = numbers.iter().sum();
assert_eq!(sum, 15);
let max = numbers.iter().max();
assert_eq!(max, Some(&7));
Query Consumers
These methods answer a single question about the iterator:
any(|x| condition)— returnstrueif at least one element satisfies the predicate.all(|x| condition)— returnstrueif every element satisfies the predicate (or if the iterator is empty — vacuous truth).find(|x| condition)— returnsSome(first_match)orNone.position(|x| condition)— returnsSome(index_of_first_match)orNone. Requires the iterator to implementExactSizeIterator.count()— returns the number of elements produced by the iterator. Consumes the entire iterator.
Infinite Iterator + count = Hang:
Calling count() (or collect(), sum(), fold(), etc.) on an infinite iterator like 1.. will loop forever. The compiler will not prevent this. Always ensure the iterator is bounded before using an unbounded consumer.
Side-Effect Consumer
for_each(|x| { ... }) runs the closure for each element, discarding the return value. It is the functional equivalent of a for loop and is appropriate when the entire purpose is a side effect — printing, sending a network message, updating an external counter. Prefer a for loop when you need early exit with break, because for_each cannot be broken out of.
Implementing Your Own Iterators
When the built-in iterator constructors and adapters are not enough, you can build an iterator from scratch by defining a new type and implementing the Iterator trait for it.
Step 1: Define a struct that holds the iteration state
Decide what internal state the iterator needs to track. This could be an index, a reference to a data structure, a counter, or any combination. The struct will be the concrete iterator type.
struct Range {
current: u32,
end: u32,
}
Step 2: Implement the Iterator trait
Implement Iterator for your struct. Set the associated type Item to the type of value the iterator will yield, then write fn next(&mut self) -> Option<Self::Item>. The next method must advance the state and return the next element, or None when there are none left.
impl Iterator for Range {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
let val = self.current;
self.current += 1;
Some(val)
} else {
None
}
}
}
Step 3: Provide a constructor
Add a function (often named new) that creates a fresh instance of the iterator with its initial state.
impl Range {
fn new(start: u32, end: u32) -> Self {
Range { current: start, end }
}
}
Now the iterator is usable manually:
let mut range = Range::new(1, 4);
assert_eq!(range.next(), Some(1));
assert_eq!(range.next(), Some(2));
assert_eq!(range.next(), Some(3));
assert_eq!(range.next(), None);
Step 4: Make it work in for loops with IntoIterator
To use a type directly in for x in my_range syntax, you need to implement IntoIterator — usually for the wrapper type that holds the data, not for the iterator itself. The wrapper's into_iter method returns the iterator.
struct EvenNumbers {
max: u32,
}
struct EvenNumbersIter {
current: u32,
max: u32,
}
impl Iterator for EvenNumbersIter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current > self.max { return None; }
let val = self.current;
self.current += 2;
Some(val)
}
}
impl IntoIterator for EvenNumbers {
type Item = u32;
type IntoIter = EvenNumbersIter;
fn into_iter(self) -> Self::IntoIter {
EvenNumbersIter { current: 0, max: self.max }
}
}
// Now you can write:
for n in EvenNumbers { max: 10 } {
println!("{}", n);
}
Your Iterator Works if It Behaves Like This:
A correctly implemented iterator will: produce the expected elements in order when you call next repeatedly; return None after the sequence ends; continue returning None on subsequent calls (or restarts only if the design intentionally wraps around). If your type meets these criteria, every adapter and consumer method in the standard library will work with it automatically.
Common pitfalls include forgetting to update the state inside next, which causes an infinite loop, or accidentally returning Some after exhaustion, which can cause logic errors downstream. Treat the first implementation as a state machine — use assert! tests to verify behavior before relying on complex adapter chains.
Use Iterator Transforms Instead of Explicit Loops
Explicit loops with mutable accumulators and if branches are familiar, but iterator chains often express the same intent more clearly and with fewer moving parts.
Compare the following two approaches that filter even numbers, square them, and collect the result.
Imperative style:
let numbers = vec![1, 2, 3, 4, 5, 6];
let mut result = Vec::new();
for &n in &numbers {
if n % 2 == 0 {
result.push(n * n);
}
}
Iterator style:
let result: Vec<i32> = numbers.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.collect();
The iterator version declares what the code should produce, without the mechanical details of pushing into a mutable vector. The type of result is deduced from collect, and the compiler optimizes both versions to essentially the same machine code.
Iterator chains are not always the right answer. When the logic involves complex early exits, interacting with external state inside the loop, or deeply nested conditionals, a for loop may be easier to follow. Use for_each for side effects when you don't need to break, and reach for fold or reduce when the accumulator itself is the output.
A pragmatic guideline: if you find yourself reaching for a mutable local variable just to carry a value across loop iterations, an iterator combinator like fold or collect can often replace it.
Summary
The iterator system is a cohesive whole, not a loose bag of methods. IntoIterator makes a type traversable, Iterator defines the single method that does the traversal, and the adapter/consumer split separates constructing a plan from executing it. That separation is what gives iterators their laziness and composability.
The single most important insight is that every for loop, every .map(), and every .collect() is ultimately calling .next() in a loop. Once you internalize that, the entire system becomes predictable: adapters are just wrappers that redefine what .next() means, and consumers are just functions that call .next() until they get None.