The Iterator Trait
An iterator is a value that knows how to produce a sequence of items, one at a time, on demand. In Rust, this idea is not built into the language as special syntax — it is a single trait, Iterator, with one required method. Everything else you associate with iteration in Rust — for loops, map, filter, collect, and dozens of other tools — is built on top of that one method. Understanding this trait is the key to writing idiomatic, fast Rust code instead of hand-rolled index loops.
Overview: How Iterators Work
At its core, the Iterator trait looks like this: it has an associated type Item (the kind of value it yields) and one required method, next, which returns Option<Self::Item>. Calling next() either returns Some(value) — here is the next item, and the iterator’s internal state has advanced — or None, meaning the sequence is exhausted. That is the entire contract. There is no separate "has more items" check; the Option return value does both jobs at once.
Once a type implements just that one method, it automatically gets dozens of other methods for free — map, filter, zip, take, fold, sum, collect, and more — because the Iterator trait provides them as default methods implemented purely in terms of next(). This is why implementing Iterator for your own type is so cheap: you write five or ten lines for next(), and the standard library hands you an entire toolkit.
The second crucial idea is laziness. Calling v.iter().map(|x| x * 2) does not loop over anything yet — it just builds a new iterator value that remembers the mapping function and wraps the original iterator. No work happens until something actually pulls values out by calling next(), either directly, through a for loop, or through a consuming method like collect() or sum(). Methods that return another iterator (like map and filter) are called adapters; methods that drive the iteration to completion and produce a final value are called consumers. A chain of adapters followed by one consumer compiles down to a single tight loop with no intermediate allocations — this is Rust’s "zero-cost abstraction" promise in action.
Finally, there is a related trait, IntoIterator, which is what actually powers the for loop. A for x in collection loop desugars to calling collection.into_iter() and then repeatedly calling .next() on the result until it returns None. Most collections offer three flavors of this: .iter() yields shared references (&T), .iter_mut() yields mutable references (&mut T), and .into_iter() yields owned values (T), consuming the collection in the process. Choosing the right one matters — using into_iter() on a Vec moves it, so you cannot use that vector afterward.
Syntax
The trait itself is defined (in simplified form) like this:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// plus dozens of default methods built on top of next(),
// such as map, filter, zip, take, fold, sum, and collect
}
type Item— an associated type declaring what kind of value this iterator produces.fn next(&mut self) -> Option<Self::Item>— the single required method; it takes a mutable reference because advancing the iterator changes its internal state.- Every other method (
map,filter,collect, …) has a default implementation written in terms ofnext(), so implementers only need to supplynext().
Some of the most common methods you will reach for:
| Method | Category | What it does |
|---|---|---|
next() |
required | Returns the next item as Some(item), or None when exhausted |
map(f) |
adapter (lazy) | Transforms each item using a closure |
filter(pred) |
adapter (lazy) | Keeps only items for which pred returns true |
zip(other) |
adapter (lazy) | Pairs items from two iterators, stopping at the shorter one |
enumerate() |
adapter (lazy) | Pairs each item with its index, starting at 0 |
take(n) / skip(n) |
adapter (lazy) | Limits to the first n items, or skips the first n |
collect() |
consumer | Builds a collection (Vec, String, HashMap, …) from the items |
sum() / count() / fold() |
consumer | Reduces the whole sequence to a single value |
for_each(f) |
consumer | Runs a closure on every item, discarding the results |
Examples
Example 1: Calling next() by hand
Before reaching for adapters, it helps to see the raw mechanism. Here we get an iterator with .iter() and pull items out manually, then let a for loop do the same work automatically.
fn main() {
let numbers = vec![10, 20, 30];
let mut iter = numbers.iter();
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
for n in &numbers {
println!("value: {}", n);
}
}
Output:
Some(10)
Some(20)
Some(30)
None
value: 10
value: 20
value: 30
Each call to next() returns Some(&i32) until the vector is exhausted, at which point it returns None forever. The for loop below does exactly the same thing internally — it just hides the repeated next() calls.
Example 2: A custom iterator
Implementing Iterator for your own type only requires writing next(); every adapter and consumer method becomes available automatically.
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
println!("sum = {}", sum);
let collected: Vec<u32> = Counter::new().collect();
println!("{:?}", collected);
}
Output:
sum = 18
[1, 2, 3, 4, 5]
Counter::new() yields 1, 2, 3, 4, 5. The second counter, after .skip(1), yields 2, 3, 4, 5. zip pairs them into (1,2), (2,3), (3,4), (4,5); map multiplies each pair to get 2, 6, 12, 20; filter keeps only multiples of 3, leaving 6 and 12; and sum adds them to 18. None of this ran until .sum() was called — the chain of adapters was just a lazy plan until then. Notice we never wrote map, zip, filter, sum, or collect ourselves; Counter got all of them by implementing only next().
Example 3: Filtering and mapping real data
A more realistic case: pulling long words out of a list and uppercasing them, plus computing a total.
fn main() {
let words = vec![
String::from("rust"),
String::from("is"),
String::from("blazingly"),
String::from("fast"),
];
let long_words: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();
println!("{:?}", long_words);
let total_chars: usize = words.iter().map(|w| w.len()).sum();
println!("total characters: {}", total_chars);
}
Output:
["RUST", "BLAZINGLY", "FAST"]
total characters: 19
Using .iter() instead of .into_iter() is important here: it borrows each String rather than moving it, so words is still fully usable on the next line for the total_chars calculation. If we had used words.into_iter() in the first pipeline, words would have been moved and the second line would fail to compile.
How It Works Step by Step
When the compiler sees for x in v { ... }, it does not treat this as magic syntax. It desugars roughly to the following, using IntoIterator::into_iter and a while let loop around next():
let v = vec![1, 2, 3];
let mut iter = v.into_iter();
while let Some(x) = iter.next() {
println!("{}", x);
}
Output:
1
2
3
Step by step: v.into_iter() consumes the vector and produces an iterator that owns its elements. Each loop iteration calls iter.next(): while it returns Some(x), the loop body runs with x bound to that value; the moment it returns None, the loop ends. This is exactly what a for loop does under the hood — there is no separate looping construct in the compiler for collections, just this one pattern applied to anything implementing IntoIterator. Adapter chains work the same way: each adapter’s next() pulls from the iterator underneath it, transforms or filters the result, and passes it up, all driven by a single outer call to next() from the final consumer. Because this all monomorphizes at compile time (no dynamic dispatch involved for concrete adapter chains), the resulting machine code is typically as fast as a hand-written loop.
Common Mistakes
Mistake 1: Using a collection after a consuming for loop
Writing for x in v (rather than for x in &v) calls v.into_iter(), which moves v. Trying to use v afterward is a compile error, not a runtime bug — the borrow checker catches it before the program ever runs.
fn main() {
let v = vec![1, 2, 3];
for x in v {
println!("{}", x);
}
println!("{:?}", v); // error: value borrowed here after move
}
The fix is to iterate by reference with &v (or v.iter()), which borrows each element instead of taking ownership, leaving v valid afterward:
fn main() {
let v = vec![1, 2, 3];
for x in &v {
println!("{}", x);
}
println!("{:?}", v);
}
Mistake 2: Forgetting mut on an iterator binding
Calling .next() requires &mut self, because advancing an iterator changes its internal position. If the iterator variable itself is not declared mut, this fails to compile.
fn main() {
let v = vec![1, 2, 3];
let iter = v.iter();
println!("{:?}", iter.next()); // error: cannot borrow `iter` as mutable
}
Adding mut to the binding fixes it:
fn main() {
let v = vec![1, 2, 3];
let mut iter = v.iter();
println!("{:?}", iter.next());
}
Note that this only comes up when you call .next() directly; a for loop never requires you to write mut yourself, because the desugared code introduces its own hidden mut binding for you.
Best Practices
- Prefer
.iter()for read-only access,.iter_mut()when you need to modify elements in place, and.into_iter()only when you genuinely want to consume and own the values. - Chain adapters (
map,filter,zip, …) instead of writing manual index-based loops — they are just as fast and far less error-prone (no off-by-one bugs). - Remember that adapters are lazy: a chain that ends without a consumer like
collect(),sum(),for_each(), or aforloop does nothing at all. - Use
enumerate()instead of maintaining a manual counter variable when you need both the index and the value. - When implementing your own type’s
Iterator, keepnext()simple and correct; you get the entire adapter/consumer toolkit for free. - Reach for
collect()only when you actually need the resulting collection stored; if you just need to process items once, iterate directly without collecting first.
Practice Exercises
- Given
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];, use iterator adapters to compute the sum of the squares of only the even numbers. Expected result:220. - Implement your own
Iteratorfor a struct calledFibonaccithat yields the Fibonacci sequence (0, 1, 1, 2, 3, 5, …). Use.take(8).collect::<Vec<u64>>()to print the first eight values. - Given
let names = vec!["Ann", "Bo", "Charlie", "Dee"];, useenumerate()and aforloop to print each name alongside its 1-based position, e.g.1: Ann.
Summary
- The
Iteratortrait requires only one method,next(&mut self) -> Option<Self::Item>, and provides dozens of adapter and consumer methods for free on top of it. forloops are not special syntax for collections — they desugar toIntoIterator::into_iter()followed by repeated calls tonext().- Adapters like
map,filter, andzipare lazy and build a pipeline; consumers likecollect,sum, andfor_eachactually drive iteration. - Use
.iter()for borrowed access,.iter_mut()for mutable borrowed access, and.into_iter()for owned, moving access — picking the wrong one is a common source of move errors. - Calling
.next()directly requires amutbinding, since advancing an iterator mutates its internal state. - Implementing
Iteratorfor your own types is cheap and gives you the full standard-library iterator toolkit automatically.
