Site

Iterators — Rust Iterator Pattern

Tutorial 9.0  •  Rust / Learn

9.0 What This Teaches

Rust's iterator pattern lets you process sequences of values in a composable, lazy way without writing manual index loops. This tutorial covers:

9.1 The Iterator Trait

The Iterator trait requires exactly one method: next(). It returns Some(item) while items remain and None when the sequence is exhausted. Every other method on iterators - map, filter, collect, and dozens more - is provided automatically, built on top of next().
// Iterator trait - simplified view
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // all other methods are provided defaults built on next()
}
type Item is an associated type - it declares what kind of value the iterator yields. A Vec<i32> iterator yields i32 (or references to it, depending on how you create the iterator).

9.2 Creating Iterators

Collections provide three methods for creating iterators, each with a different ownership relationship to the source data:
// Iterators - creating iterators from a Vec
let v = vec![1, 2, 3];

for x in v.iter() {         // x: &i32
    println!("{x}");
}
println!("v still here: {v:?}");  // v is still owned here

let mut w = vec![1, 2, 3];
for x in w.iter_mut() {     // x: &mut i32
    *x *= 2;
}
println!("{w:?}");           // [2, 4, 6]

for x in v.into_iter() {    // x: i32, v is consumed
    println!("{x}");
}
// v cannot be used here

9.3 Consuming Adaptors

A consuming adaptor drives the iterator to completion and produces a single result. After a consuming adaptor runs, the iterator is spent and cannot be reused.
// Iterators - consuming adaptors
let nums = vec![1, 2, 3, 4, 5];

let total: i32 = nums.iter().copied().sum();   // 15
let count = nums.iter().count();               // 5
let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();
nums.iter().for_each(|x| println!("{x}"));
collect() is the most flexible consumer - it can gather items into a Vec, HashMap, String, or any other collection that implements FromIterator. The type annotation tells Rust which collection to build.

9.4 map and filter

map transforms each element by applying a closure. filter keeps only elements where the closure returns true. Both are lazy - they produce a new iterator type and do no work until a consuming adaptor pulls from the chain.
// Iterators - map and filter
let nums = vec![1, 2, 3, 4, 5, 6];

let evens_doubled: Vec<i32> = nums.iter()
    .filter(|&&x| x % 2 == 0)   // keep even elements
    .map(|&x| x * 2)             // double each one
    .collect();

println!("{evens_doubled:?}");   // [4, 8, 12]
The double reference pattern |&&x| in the filter closure arises because .iter() yields &i32, and filter passes a reference to that, giving &&i32. Destructuring both levels with &&x gives the plain i32. Using .copied() before filter avoids this.
// Iterators - using copied() to avoid double-reference patterns
let evens_doubled: Vec<i32> = nums.iter()
    .copied()                    // &i32 -> i32
    .filter(|&x| x % 2 == 0)
    .map(|x| x * 2)
    .collect();                  // [4, 8, 12]

9.5 enumerate and zip

enumerate() pairs each element with its index, yielding (usize, &T) tuples. zip() combines two iterators element-by-element into pairs, stopping when the shorter one is exhausted.
// Iterators - enumerate and zip
let words = vec!["alpha", "beta", "gamma"];

for (i, w) in words.iter().enumerate() {
    println!("{i}: {w}");
}
// 0: alpha
// 1: beta
// 2: gamma

let nums   = vec![1, 2, 3];
let letters = vec!['a', 'b', 'c'];
let pairs: Vec<_> = nums.iter().zip(letters.iter()).collect();
println!("{pairs:?}");   // [(1, 'a'), (2, 'b'), (3, 'c')]

9.6 Chaining Adaptors

Adaptors compose naturally. Each one wraps the previous iterator in a new type; the entire chain stays lazy until a consuming adaptor is called. Rust's type system and optimizer collapse the chain into efficient code with no intermediate allocations.
// Iterators - chaining multiple adaptors
let data = vec![3, 1, 4, 1, 5, 9, 2, 6];

let result: Vec<i32> = data.iter()
    .copied()                    // &i32 -> i32
    .filter(|&x| x > 3)         // keep values above 3
    .map(|x| x * x)              // square each value
    .collect();

println!("{result:?}");   // [16, 25, 81, 36]
No intermediate Vec is created between steps. Each element flows through the full chain one at a time, allocated to the final Vec only when collect() runs.

9.7 Implementing Iterator

Any struct can become an iterator by implementing Iterator. Only next() is required. All adaptor and consumer methods become available automatically because they are default methods on the trait.
// Iterators - custom iterator that counts from 1 up to a limit
struct Counter {
    current: u32,
    limit: u32,
}

impl Counter {
    fn new(limit: u32) -> Self {
        Counter { current: 0, limit }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.limit {
            self.current += 1;
            Some(self.current)
        } else {
            None
        }
    }
}

// all Iterator methods are now available for free
let values: Vec<u32> = Counter::new(5).collect();
println!("{values:?}");       // [1, 2, 3, 4, 5]

let total: u32 = Counter::new(5).sum();
println!("{total}");           // 15

let squares: Vec<u32> = Counter::new(4).map(|x| x * x).collect();
println!("{squares:?}");      // [1, 4, 9, 16]

9.8 Example - All Together

// Iterators - demonstrates creating, adapting, and consuming iterators in Rust.

struct Counter {
    current: u32,
    limit: u32,
}

impl Counter {
    fn new(limit: u32) -> Self {
        Counter { current: 0, limit }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.limit {
            self.current += 1;
            Some(self.current)
        } else {
            None
        }
    }
}

fn main() {
    // sum via copied() + sum()
    let v = vec![1, 2, 3, 4, 5];
    let total: i32 = v.iter().copied().sum();
    println!("sum = {total}");

    // filter + map + collect
    let evens_doubled: Vec<i32> = v.iter()
        .copied()
        .filter(|&x| x % 2 == 0)
        .map(|x| x * 2)
        .collect();
    println!("evens doubled = {evens_doubled:?}");

    // enumerate
    let words = vec!["alpha", "beta", "gamma"];
    for (i, w) in words.iter().enumerate() {
        println!("{i}: {w}");
    }

    // zip
    let nums    = vec![1, 2, 3];
    let letters = vec!['a', 'b', 'c'];
    let pairs: Vec<_> = nums.iter().zip(letters.iter()).collect();
    println!("{pairs:?}");

    // chaining
    let data = vec![3, 1, 4, 1, 5, 9, 2, 6];
    let result: Vec<i32> = data.iter()
        .copied()
        .filter(|&x| x > 3)
        .map(|x| x * x)
        .collect();
    println!("{result:?}");

    // custom iterator
    let csum: u32 = Counter::new(5).sum();
    println!("counter sum = {csum}");
}
Expected output:
sum = 15
evens doubled = [4, 8, 12]
0: alpha
1: beta
2: gamma
[(1, 'a'), (2, 'b'), (3, 'c')]
[16, 25, 81, 36]
counter sum = 15

9.9 Exercise

Exercise
  • Count how many numbers in a Vec<i32> are divisible by 3 using .iter().copied().filter(...).count().
  • Convert a Vec<&str> into a Vec<String> using .map(|s| s.to_string()).collect().
  • Implement a Fibonacci iterator that yields the Fibonacci sequence indefinitely. Collect the first 10 values with .take(10).collect::<Vec<_>>().

9.10 Common Mistakes

Consuming the same iterator twice

let v = vec![1, 2, 3];
let it = v.iter();
let s: i32 = it.copied().sum();
let c = it.count();   // compile error: value used after move
A consuming adaptor takes ownership of the iterator. Create a fresh iterator for each use.

Forgetting collect() - nothing runs

let v = vec![1, 2, 3];
v.iter().map(|&x| x * 2);   // does nothing - no consumer
Adaptors are lazy. Without .collect(), .sum(), or another consumer at the end, the chain never executes.

Double-reference pattern in filter closures

let v = vec![1, 2, 3];
// iter() yields &i32; filter adds another &, giving &&i32
v.iter().filter(|x| *x > 1);     // x is &&i32, must deref
v.iter().filter(|&&x| x > 1);    // destructure both layers
v.iter().copied().filter(|&x| x > 1);  // or use copied() first
Use .copied() before filter to work with plain values.

Using into_iter() when you still need the collection

let v = vec![1, 2, 3];
let _s: i32 = v.into_iter().sum();
println!("{v:?}");   // compile error: value borrowed after move
into_iter() consumes the collection. Use .iter().copied().sum() if you need v afterward.

9.11 Key Terms

TermMeaning
IteratorTrait with next() and all adaptor/consumer methods; core of Rust's iteration model
ItemAssociated type on Iterator; the element type returned by next()
iter()Creates a borrowing iterator yielding &T; source collection remains usable
iter_mut()Creates a mutable-borrowing iterator yielding &mut T; allows in-place mutation
into_iter()Consumes the collection and yields owned T values
adaptorMethod that transforms one iterator into another; lazy - does no work until consumed
consumerMethod that drives the iterator chain to completion (sum, collect, for_each)
lazy evaluationAdaptors produce no output until a consumer pulls from the chain
copied()Adaptor that converts &T to T for Copy types, simplifying closures
collect()Consumer that gathers iterator output into a Vec, HashMap, or any FromIterator type