Skip to main content
Rust intermediate Lesson 18 of 30

Iterators in Rust

Understand the Iterator trait, lazy evaluation, adapter chaining, and how to write custom iterators.

The Iterator Trait

Iterators are the idiomatic way to process sequences of values in Rust. Rather than writing index-based for loops, you compose adapters that describe what transformations to apply. This style is not just cleaner — the compiler can often optimize a chain of adapters into a single tight loop with no intermediate allocations. Everything in Rust’s iteration system is built on one simple trait:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 70+ methods with default implementations built on top of next()
}

When next() returns None, iteration is complete. All other iterator methods — map, filter, zip, sum, and so on — have default implementations that call next(), so you get them for free by implementing just one method.

The Three Iteration Methods

Collections expose three different ways to iterate, giving you control over ownership. The right choice depends on whether you need to keep using the collection afterwards, whether you need to mutate elements, or whether you want to consume and transform the values.

fn main() {
    let v = vec![1, 2, 3];

    // iter() — borrows the collection, yields &T, collection remains usable
    for x in v.iter() {
        print!("{} ", x); // x is &i32
    }
    println!();

    // iter_mut() — mutably borrows, yields &mut T, allows in-place modification
    let mut v2 = vec![1, 2, 3];
    for x in v2.iter_mut() {
        *x *= 2; // double each element in place
    }
    println!("{:?}", v2); // [2, 4, 6]

    // into_iter() — consumes the collection, yields owned T values
    let v3 = vec![1, 2, 3];
    for x in v3.into_iter() {
        print!("{} ", x); // x is i32 (owned)
    }
    println!();
    // v3 is moved; cannot be used after this point
}

In a for loop, for x in collection calls into_iter() implicitly.

Lazy Evaluation

Iterator adapters are lazy — they describe what to do but do no actual work until a consuming method drives them. This means you can build up a long pipeline without allocating intermediate collections, and you can even describe infinite sequences and only compute as many elements as you need.

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Building the pipeline does no computation — it just creates iterator structs
    let pipeline = v.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x);

    // collect() is the consuming adapter that drives the pipeline and allocates the result
    let result: Vec<i32> = pipeline.collect();
    println!("{:?}", result); // [4, 16, 36, 64, 100]
}

Laziness enables infinite sequences — take stops the pipeline after enough elements are produced:

fn main() {
    // Fibonacci as an infinite iterator — no allocation until take() bounds it
    let fibs = std::iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b)))
        .map(|(a, _)| a);

    let first_ten: Vec<u64> = fibs.take(10).collect();
    println!("{:?}", first_ten); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}

Common Adapters

Transforming: map, flat_map, flatten

map transforms each element independently. flat_map transforms each element into an iterator and then concatenates all those iterators — useful when each input produces multiple outputs. flatten does just the concatenation step if you already have an iterator of iterators.

fn main() {
    let words = vec!["hello world", "foo bar"];

    // map — one-to-one transformation
    let upper: Vec<String> = words.iter().map(|s| s.to_uppercase()).collect();
    println!("{:?}", upper);

    // flat_map — one-to-many: each string produces multiple words
    let individual: Vec<&str> = words.iter()
        .flat_map(|s| s.split_whitespace())
        .collect();
    println!("{:?}", individual); // ["hello", "world", "foo", "bar"]

    // flatten — collapses one level of nesting
    let nested = vec![vec![1, 2], vec![3, 4]];
    let flat: Vec<i32> = nested.into_iter().flatten().collect();
    println!("{:?}", flat); // [1, 2, 3, 4]
}

Filtering: filter, filter_map, take_while, skip_while

filter keeps elements matching a predicate. filter_map combines filtering and transformation in one step — elements that map to None are discarded, elements that map to Some(v) produce v. This is the idiomatic way to parse or convert a sequence while skipping invalid entries.

fn main() {
    let numbers = vec!["1", "two", "3", "four", "5"];

    // filter_map — parse each string, discard parse failures (None), keep successes
    let parsed: Vec<i32> = numbers.iter()
        .filter_map(|s| s.parse().ok())
        .collect();
    println!("{:?}", parsed); // [1, 3, 5]

    let data = vec![2, 4, 6, 1, 8, 10];

    // take_while — yields elements until the predicate first fails, then stops
    let prefix: Vec<i32> = data.iter().copied().take_while(|&x| x % 2 == 0).collect();
    println!("{:?}", prefix); // [2, 4, 6]

    // skip_while — skips elements until the predicate first fails, then yields the rest
    let rest: Vec<i32> = data.iter().copied().skip_while(|&x| x % 2 == 0).collect();
    println!("{:?}", rest); // [1, 8, 10]
}

Slicing: take, skip, step_by

These adapters let you work with a portion of a sequence without materializing the whole thing first. skip + take is the natural way to implement pagination over any iterator.

fn main() {
    let v: Vec<i32> = (1..=20).collect();

    // Page 2 of size 5 — skip the first 5, take the next 5
    let page2: Vec<i32> = v.iter().copied().skip(5).take(5).collect();
    println!("{:?}", page2); // [6, 7, 8, 9, 10]

    // Every third element
    let every_third: Vec<i32> = v.iter().copied().step_by(3).collect();
    println!("{:?}", every_third); // [1, 4, 7, 10, 13, 16, 19]
}

Combining: zip, chain, enumerate

zip pairs up elements from two iterators — when either runs out, iteration stops. chain concatenates two iterators into one. enumerate adds a running index to any iterator, avoiding manual counter variables.

fn main() {
    let names = vec!["Alice", "Bob", "Charlie"];
    let scores = vec![95, 87, 92];

    // zip — pair corresponding elements from two iterators
    let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
    println!("{:?}", pairs);

    // enumerate — (index, value) without a manual counter
    for (i, name) in names.iter().enumerate() {
        println!("{}: {}", i + 1, name);
    }

    // chain — treat two iterators as one sequential stream
    let a = vec![1, 2, 3];
    let b = vec![4, 5, 6];
    let combined: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
    println!("{:?}", combined); // [1, 2, 3, 4, 5, 6]
}

Consuming Adapters

Consuming adapters drive the iterator to completion and return a single result. They are the “trigger” that actually runs the pipeline. Common ones include collect, sum, fold, any, all, and find.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    println!("{}", v.iter().sum::<i32>());              // 15
    println!("{}", v.iter().product::<i32>());          // 120
    println!("{}", v.iter().count());                   // 5
    println!("{:?}", v.iter().min());                   // Some(1)
    println!("{:?}", v.iter().max());                   // Some(5)
    println!("{}", v.iter().any(|&x| x > 3));           // true — stops at first match
    println!("{}", v.iter().all(|&x| x > 0));           // true — stops at first failure
    println!("{:?}", v.iter().find(|&&x| x > 3));       // Some(4)
    println!("{:?}", v.iter().position(|&x| x > 3));    // Some(3) — index of first match

    // fold — general reduction with an accumulator; the basis for sum, product, etc.
    let factorial = (1..=5u64).fold(1, |acc, x| acc * x);
    println!("{}", factorial); // 120

    // for_each — like a for loop but chainable in a pipeline
    v.iter().for_each(|&x| print!("{} ", x));
    println!();
}

Custom Iterators

Implementing Iterator on your own type makes it compatible with every adapter in the standard library. You define type Item and next() — the rest comes for free. This is one of the most powerful demonstrations of Rust’s trait system: a small interface contract unlocks an enormous amount of behavior.

// Iterates over every (x, y) coordinate in a 2D grid, row by row
struct Range2D {
    width: usize,
    height: usize,
    cx: usize,
    cy: usize,
}

impl Range2D {
    fn new(width: usize, height: usize) -> Self {
        Self { width, height, cx: 0, cy: 0 }
    }
}

impl Iterator for Range2D {
    type Item = (usize, usize);

    // Define next() — all 70+ other Iterator methods are automatically available
    fn next(&mut self) -> Option<(usize, usize)> {
        if self.cy >= self.height { return None; }
        let item = (self.cx, self.cy);
        self.cx += 1;
        if self.cx >= self.width {
            self.cx = 0;
            self.cy += 1;
        }
        Some(item)
    }
}

fn main() {
    for (x, y) in Range2D::new(3, 2) {
        println!("({}, {})", x, y);
    }
    // (0,0) (1,0) (2,0) (0,1) (1,1) (2,1)

    // filter, count, and every other adapter work automatically
    let diagonal_count = Range2D::new(4, 4).filter(|(x, y)| x == y).count();
    println!("diagonal cells: {}", diagonal_count); // 4
}

std::iter Constructors

The std::iter module provides functions for building common iterator patterns from scratch, without needing an existing collection to iterate over.

fn main() {
    // repeat — infinite repetition of one value; always pair with take()
    let threes: Vec<i32> = std::iter::repeat(3).take(5).collect();
    println!("{:?}", threes); // [3, 3, 3, 3, 3]

    // once — a single-element iterator; useful for prepending to a chain
    let one: Vec<&str> = std::iter::once("hello").collect();
    println!("{:?}", one); // ["hello"]

    // empty — zero elements; useful as a neutral element in chains
    let none: Vec<i32> = std::iter::empty().collect();
    println!("{:?}", none); // []

    // from_fn — generate elements with a closure that returns Option<T>
    let mut n = 0u64;
    let powers: Vec<u64> = std::iter::from_fn(move || {
        n += 1;
        Some(n * n)
    }).take(5).collect();
    println!("{:?}", powers); // [1, 4, 9, 16, 25]
}

Frequently Asked Questions

Are Rust iterators lazy?
Yes. Iterator adapters like map and filter are lazy — they do not execute until a consuming adapter like collect, sum, or for_each is called.
What is the difference between iter(), iter_mut(), and into_iter()?
iter() yields shared references &T. iter_mut() yields mutable references &mut T. into_iter() consumes the collection and yields owned values T.
How do I implement my own iterator?
Implement the Iterator trait on your type by defining the associated type Item and the next() method. All other iterator methods are provided for free.