Iterators in Rust: Adapters & Custom Iterator Guide

Max WellsMax WellsFounder of Rustify

TL;DR: An iterator in Rust is any type that implements the Iterator trait, 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:

AdapterWhat 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


  • Closure: Closures are the primary argument to iterator adapters
  • Trait: Iterator is 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::Item is 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

Ready to Land a $120k+ Rust Job in the US or Europe?