TL;DR: An iterator in Rust is any type that implements the
Iteratortrait, which requires a single method:next() -> Option<Self::Item>. Iterators are lazy, they do no work until consumed. You chain adapters like.map(),.filter(), and.take()to transform sequences, then consume with.collect(),.for_each(), or.fold(). The entire chain compiles down to a single loop, no intermediate allocations, equivalent in performance to hand-written C loops.
What Is an Iterator in Rust?
An iterator is any type implementing the Iterator trait, a protocol for producing a sequence of values one at a time, on demand.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 70+ default methods built on top of next()
}Any type with a next() method that returns Option<Item> gets all 70+ iterator methods for free, .map(), .filter(), .zip(), .enumerate(), .flat_map(), and many more. This is trait-based extension at its most powerful.
How Do You Get an Iterator?
Collections expose iterators through .iter() (borrows), .iter_mut() (mutable borrows), and .into_iter() (consumes the collection).
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// .iter(); yields &i32, collection is still usable afterward
for n in numbers.iter() {
println!("{n}");
}
// .into_iter(); yields i32, consumes the vec
let doubled: Vec<i32> = numbers.into_iter().map(|x| x * 2).collect();
// Ranges are iterators too
let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
println!("{:?}", squares); // [1, 4, 9, 16, 25]
}for x in collection desugars to for x in collection.into_iter(), the for loop is syntactic sugar over Iterator::next().
How Do Iterator Adapters Work?
Adapters transform one iterator into another, they are lazy and do no work until consumed. Chaining adapters builds a description of computation, not the result.
fn main() {
let data = vec!["hello", "world", "rust", "is", "fast"];
// Nothing executes here; just building the chain
let pipeline = data
.iter()
.filter(|s| s.len() > 3) // lazy filter
.map(|s| s.to_uppercase()) // lazy transform
.enumerate(); // lazy index pairing
// .collect() drives the chain; single pass, no intermediate Vecs
let result: Vec<(usize, String)> = pipeline.collect();
println!("{:?}", result);
// [(0, "HELLO"), (1, "WORLD"), (2, "RUST"), (3, "FAST")]
}Common adapters:
| Adapter | What it does |
|---|---|
.map(f) | Transform each item with f |
.filter(pred) | Keep items where pred returns true |
.filter_map(f) | Transform and filter in one step (returns Option) |
.flat_map(f) | Map then flatten nested iterators |
.take(n) | First n items only |
.skip(n) | Skip first n items |
.enumerate() | Pair each item with its index (i, item) |
.zip(other) | Pair items from two iterators |
.chain(other) | Concatenate two iterators |
.peekable() | Look at the next item without consuming it |
How Do You Consume an Iterator?
Consumers drive the lazy chain to completion, they call next() internally until None is returned.
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// collect; gather into a collection
let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();
// fold; reduce to a single value
let sum = nums.iter().fold(0, |acc, &x| acc + x);
// for_each; side effects only, returns ()
nums.iter().for_each(|x| print!("{x} "));
// any / all; short-circuit boolean checks
let has_even = nums.iter().any(|&x| x % 2 == 0); // true
let all_pos = nums.iter().all(|&x| x > 0); // true
// find; first matching item
let first_even = nums.iter().find(|&&x| x % 2 == 0); // Some(&2)
// count; consume and count
let count = nums.iter().filter(|&&x| x > 2).count(); // 3
}How Do You Write a Custom Iterator?
Implement Iterator on any type by defining next(), you get all adapter methods automatically.
struct Fibonacci {
a: u64,
b: u64,
}
impl Fibonacci {
fn new() -> Self { Self { a: 0, b: 1 } }
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let next = self.a + self.b;
self.a = self.b;
self.b = next;
Some(self.a) // infinite; never returns None
}
}
fn main() {
let fibs: Vec<u64> = Fibonacci::new().take(8).collect();
println!("{:?}", fibs); // [1, 1, 2, 3, 5, 8, 13, 21]
}Frequently Asked Questions
Yes. The compiler monomorphizes each iterator chain and inlines closures, producing a single loop with no heap allocations for intermediate steps. LLVM further optimizes the result ; Rust iterator chains often auto-vectorize to SIMD instructions.
.iter() yields immutable references (&T): the collection is still usable after. .iter_mut() yields mutable references (&mut T): you can modify items in place. .into_iter() yields owned values (T): the collection is consumed.
use std::collections::HashMap;
let map: HashMap<&str, usize> = words.iter().map(|w| (*w, w.len())).collect();collect() is generic, the target type determines how items are assembled.
Standard Iterator is synchronous. For async iteration, use the Stream trait (from the futures crate or Tokio): it is the async equivalent of Iterator, using .next().await instead of .next().
Sources
Related Glossary Terms
- Closure: Closures are the primary argument to iterator adapters
- Trait:
Iteratoris a trait; implementing it gives you 70+ methods - Ownership:
into_iter()consumes collections;iter()borrows them - Generic: Iterator adapters are generic over the closure type
- Associated Types:
Iterator::Itemis the standard library's most important associated type - proptest: Property-testing strategies compose in iterator-like ways across generated data
- Polars: Polars' lazy expression model often feels familiar to Rust developers who already think in iterators
- Rayon: Parallel iterators are one of the clearest bridges from standard iterators to Rayon
Keep Reading
- Rust Ownership and Borrowing Explained: iterators consume or borrow their source
- Rust for Python Developers: Python generators and list comprehensions vs Rust iterators
- Rust vs Go for Backend Development: zero-cost iterator abstractions vs Go's explicit loops
