Iterators in Rust
Iterators are one of the most powerful and elegant features in Rust. They let you process sequences of values in a composable, expressive, and highly efficient way. Understanding iterators unlocks a functional programming style that compiles down to the same machine code as a hand-written loop.
The Iterator Trait
At its core, an iterator is any type that implements the Iterator trait. The trait has one required method — next — and dozens of provided methods built on top of it.
pub trait Iterator {
type Item;
// The only method you must implement
fn next(&mut self) -> Option<Self::Item>;
// All other methods (map, filter, fold, …) are provided for free
}next returns Some(item) while there are items left, and None when the sequence is exhausted. That simple contract is what makes the whole ecosystem composable.
Iterators Are Lazy
Creating an iterator does nothing by itself. No computation runs until you actually consume the iterator. This is called lazy evaluation and it is fundamental to how Rust achieves zero-cost abstractions.
let v = vec![1, 2, 3, 4, 5];
// This creates an iterator — but does no work yet
let iter = v.iter().map(|x| x * 2).filter(|x| x > &4);
// Work only happens here, when we consume it
for val in iter {
println!("{}", val);
}Creating Iterators
The standard library provides three ways to turn a collection into an iterator, and they differ in how they handle ownership.
Method | Borrows? | Item type | Collection usable after? |
|---|---|---|---|
| Immutable borrow |
| Yes |
| Mutable borrow |
| Yes (no other refs) |
| Takes ownership |
| No — moved into iterator |
let nums = vec![10, 20, 30];
// iter() — borrows, yields &i32
for n in nums.iter() {
println!("{}", n); // n is &i32
}
println!("{:?}", nums); // still usable
// iter_mut() — mutable borrow, yields &mut i32
let mut data = vec![1, 2, 3];
for n in data.iter_mut() {
*n *= 10; // dereference to modify
}
println!("{:?}", data); // [10, 20, 30]
// into_iter() — moves the vec, yields i32
let words = vec!["hello", "world"];
for w in words.into_iter() {
println!("{}", w); // w is &str (the owned item)
}
// println!("{:?}", words); // ERROR — words was movedThe for Loop Is Iterator Sugar
Rust's for loop is syntactic sugar over the Iterator trait. The compiler desugars it into an explicit loop that calls next() until None is returned.
let v = vec![1, 2, 3];
// What you write:
for x in v.iter() {
println!("{}", x);
}
// What the compiler sees:
let mut iter = v.iter();
loop {
match iter.next() {
Some(x) => println!("{}", x),
None => break,
}
}Consuming Adaptors
Methods that call next() until the iterator is empty are called consuming adaptors. After calling one of these, the iterator is gone. Below are the most commonly used ones.
collect
.collect() gathers all items into a collection. You usually need to annotate the return type so Rust knows which collection to build.
let v = vec![1, 2, 3, 4, 5];
// Collect into a Vec<i32>
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// Collect into a HashSet<i32>
use std::collections::HashSet;
let set: HashSet<i32> = v.iter().copied().collect();
println!("{:?}", set); // {1, 2, 3, 4, 5} (unordered)sum and product
let nums = vec![1, 2, 3, 4, 5];
let total: i32 = nums.iter().sum();
println!("Sum: {}", total); // 15
let product: i32 = nums.iter().product();
println!("Product: {}", product); // 120count, max, and min
let v = vec![3, 1, 4, 1, 5, 9, 2, 6];
println!("Count: {}", v.iter().count()); // 8
println!("Max: {}", v.iter().max().unwrap()); // 9
println!("Min: {}", v.iter().min().unwrap()); // 1any and all
.any() returns true if at least one element satisfies the predicate. .all() returns true only if every element satisfies the predicate. Both short-circuit: they stop consuming items as soon as the result is determined.
let scores = vec![42, 78, 95, 61, 88];
let has_perfect = scores.iter().any(|&s| s == 100);
println!("Has perfect score: {}", has_perfect); // false
let all_passing = scores.iter().all(|&s| s >= 60);
println!("All passing: {}", all_passing); // false (42 < 60)find and position
.find() returns a reference to the first element matching a predicate. .position() returns the index (as usize) of the first match.
let words = vec!["apple", "banana", "cherry", "date"];
let found = words.iter().find(|&&w| w.starts_with('c'));
println!("{:?}", found); // Some("cherry")
let idx = words.iter().position(|&w| w == "banana");
println!("{:?}", idx); // Some(1)fold
.fold(initial, closure) is the general reduction primitive. It starts with an accumulator set to initial, then calls the closure for each item — the closure receives the current accumulator and the item, and returns the new accumulator. .sum() and .product() are specialised folds.
let numbers = vec![1, 2, 3, 4, 5];
// Compute sum with fold
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {}", sum); // 15
// Build a string from words
let words = vec!["Rust", "is", "fast"];
let sentence = words.iter().fold(String::new(), |mut acc, &w| {
if !acc.is_empty() { acc.push(' '); }
acc.push_str(w);
acc
});
println!("{}", sentence); // "Rust is fast"for_each
.for_each() calls a closure on each item purely for side effects. It fits naturally at the end of an iterator chain where a for loop would require you to break the chain into a separate variable first.
let v = vec![1, 2, 3];
v.iter().for_each(|x| println!("{}", x));
// 1
// 2
// 3Implementing Your Own Iterator
Any struct that implements Iterator becomes a full first-class iterator — every adaptor in the standard library just works. Here is the classic Fibonacci sequence as a custom iterator.
struct Fibonacci {
curr: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { curr: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let result = self.curr;
let new_next = self.curr + self.next;
self.curr = self.next;
self.next = new_next;
Some(result) // Infinite — never returns None
}
}
fn main() {
// Take the first 10 Fibonacci numbers
let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", fibs);
// Sum the Fibonacci numbers below 100
let below_100: u64 = Fibonacci::new()
.take_while(|&n| n < 100)
.sum();
println!("Sum below 100: {}", below_100);
}Iterator vs For Loops
Both accomplish the same goal, but iterator chains are often more expressive for data transformation pipelines. Consider the difference between these two approaches when filtering and squaring even numbers.
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Imperative style with a for loop
let mut result = Vec::new();
for &x in &data {
if x % 2 == 0 {
result.push(x * x);
}
}
// Functional style with iterators — same result, more declarative
let result: Vec<i32> = data
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("{:?}", result); // [4, 16, 36, 64, 100]The iterator version reads like a description of what you want: keep the evens, then square them. The for loop describes how to do it step by step. Both are idiomatic Rust, but iterators excel when pipelines grow longer or need to be passed around as values.
Zero-Cost Abstractions
Rust's iterators are a prime example of the zero-cost abstraction guarantee: you do not pay for what you do not use, and you cannot hand-write it more efficiently. The compiler (via LLVM) sees through the chain of closures and fuses them into a single tight loop — often with SIMD vectorisation on top.
// This iterator chain…
let sum: i64 = (0i64..1_000_000)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();
// …compiles to roughly the same assembly as this hand-written loop:
let mut sum: i64 = 0;
let mut i: i64 = 0;
while i < 1_000_000 {
if i % 2 == 0 {
sum += i * i;
}
i += 1;
}
println!("{}", sum);Common Patterns and Gotchas
Always annotate the type when using
.collect()— Rust needs to know the target collection.Use
.copied()or.cloned()to convert&Titems toTbefore collecting..iter()yields references — remember to dereference inside closures with&xor*x.Infinite iterators like
.cycle()or custom ones must be terminated with.take(n)or a similar limiter..sum()and.product()require the item type to implementSum/Product— always true for numeric primitives.Calling a consuming adaptor twice on the same iterator will not compile — the iterator is moved on first use.
Summary
Iterators in Rust are built on a single trait with one required method (next), and the entire rich standard library API — dozens of adaptors and consumers — is derived from that. They are lazy by default, so you build up a pipeline description and pay nothing until you consume it. Custom iterators are first-class: implement the trait once and every adaptor works automatically. And thanks to the zero-cost guarantee, an elegant functional chain compiles to the same tight loop a systems programmer would write by hand.