The Iterator traitConcept
Explanation
Iterator is the trait behind every sequential walk over data in Rust —
a Vec's elements, a HashMap's entries, a range of numbers, the lines
of a file, the bytes of a socket. Underneath all of that variety sits one
tiny contract: a type implementing Iterator declares an associated
Item type and provides a single required method, next(&mut self) -> Option<Self::Item>, which produces one item at a time and returns None
once there's nothing left. Everything else — map, filter, sum,
collect, and dozens more — is a default method built entirely on top of
that one method, provided free once next exists.
This is why the trait matters as an abstraction, not just a convenience:
code written against Iterator doesn't need to know or care whether the
values are coming from a Vec, a BTreeMap, a parsed text stream, or a
value being computed on the fly. A for loop, a generic function bounded
by Iterator<Item = T>, or an adaptor chain all work identically
regardless of the source, because the source is hidden behind this one
interface. Contrast this with index-based loops (for i in 0..collection.len()), which tie the loop to a specific way of accessing
elements and don't generalize to sources that have no meaningful index at
all, like a channel receiver or a line-by-line file reader.
The mental model worth keeping is: an iterator is a stateful cursor, not
the data itself. Calling next() advances that cursor and hands back
whatever it currently points at; the underlying collection (if there is
one) is untouched by this process unless the iterator was created in a
way that consumes it. Getting hold of an iterator in the first place —
deciding whether to borrow, mutably borrow, or take ownership of the
source — is a separate concern, covered by
IntoIterator.
The trait's transformation and consumption methods split into two
families worth knowing by name: adaptors, which
wrap one iterator in another and stay lazy until
something asks for a value, and consumers,
which actually drive the cursor to produce a final result. Any type can
join this whole ecosystem for free by implementing just next — see
Custom iterators — which is also how the standard
library itself builds map, filter, and the rest, each one just
another small struct implementing Iterator around an inner iterator.
Basic usage example
let mut numbers = [10, 20, 30].iter();
assert_eq!(numbers.next(), Some(&10)); // <- next() is the trait's one required method
assert_eq!(numbers.next(), Some(&20));
Best practices & deeper information
Working with collections
Scanning a batch of sensor readings for spikes only needs the one method
every Iterator provides — the loop doesn't touch the Vec itself, only
the cursor walking across it.
let readings = vec![72.4, 68.1, 75.0, 69.9];
let mut iter = readings.iter(); // <- `iter` is a value implementing Iterator; the Vec itself is untouched
let mut spikes = 0;
while let Some(&reading) = iter.next() { // <- next() drives iteration one item at a time
if reading > 70.0 {
spikes += 1;
}
}
println!("{spikes} readings above threshold");
Why this way: every collection's .iter() hands back a type
implementing Iterator, so code that only needs next() works
identically whether the source was a Vec, a slice, or something else
entirely — the
Iterator trait docs
describe next as the trait's sole required method for exactly this
reason.
Working with text
Splitting a log line into fields produces something implementing
Iterator just like a collection would, even though no collection was
ever built.
let log_line = "2026-07-20 ERROR disk_usage_high";
let mut fields = log_line.split_whitespace(); // <- split_whitespace returns something implementing Iterator
let date = fields.next(); // <- next() pulls one field at a time, regardless of how splitting works internally
let level = fields.next();
assert_eq!(date, Some("2026-07-20"));
assert_eq!(level, Some("ERROR"));
Why this way: str methods like split_whitespace, chars, and
lines all return distinct concrete types, but every one of them
implements Iterator, so callers rely on one interface rather than
learning a bespoke API per method — the
standard library docs
document each of these as returning "an iterator."
Writing generic code
A helper that finds the highest sensor reading shouldn't care whether the
readings come from a Vec, an array, or a filtered chain — it only needs
something that implements Iterator<Item = f64>.
fn max_reading<I>(readings: I) -> Option<f64>
where
I: Iterator<Item = f64>, // <- bound directly on the Iterator trait, not on any concrete collection
{
readings.fold(None, |max, r| match max {
Some(m) if m >= r => Some(m),
_ => Some(r),
})
}
let highest = max_reading(vec![72.4, 68.1, 75.0].into_iter());
assert_eq!(highest, Some(75.0));
Why this way: bounding the parameter on Iterator<Item = f64> rather
than Vec<f64> lets the function accept any source that can produce
f64s — an array, a filtered chain, a channel receiver — without
changing its signature, the flexibility the
Book's generics chapter
recommends generic bounds for.