Skip to main content
Rust · lesson 18 of 30
Rust intermediate Lesson 18 of 30

Iterators in Rust

Understand Rust iterators: the Iterator trait, lazy evaluation traced step by step, adapter chains, consuming methods, and custom iterators.

By SkillByExample · Updated

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);

    // 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
}
$ rustc iterators_the_three_iteration_methods.rs && ./iterators_the_three_iteration_methods
1 2 3 
[2, 4, 6]
1 2 3 

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];

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

    // collect() is the consuming adapter that drives the pipeline and allocates the result
    let result: Vec<i32> = pipeline.collect();
    println!("{:?}", result);
}
$ rustc iterators_lazy_evaluation.rs && ./iterators_lazy_evaluation
pipeline built
  filter 1
  filter 2
  map    2
  filter 3
  filter 4
  map    4
[4, 16]

Nothing prints before pipeline built — the closures haven’t run yet. Then the trace shows the real execution order: each element goes all the way through filter and (if it passes) map before the next element is pulled. There is no intermediate “filtered” vector; one element at a time flows through the whole chain.

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);
}
$ rustc iterators_lazy_evaluation_2.rs && ./iterators_lazy_evaluation_2
[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);

    // 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);
}
$ rustc iterators_transforming_map_flat_map_flatten.rs && ./iterators_transforming_map_flat_map_flatten
["HELLO WORLD", "FOO BAR"]
["hello", "world", "foo", "bar"]
[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);

    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);

    // 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);
}
$ rustc iterators_filtering_filter_filter_map_take_while_s.rs && ./iterators_filtering_filter_filter_map_take_while_s
[1, 3, 5]
[2, 4, 6]
[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);

    // Every third element
    let every_third: Vec<i32> = v.iter().copied().step_by(3).collect();
    println!("{:?}", every_third);
}
$ rustc iterators_slicing_take_skip_step_by.rs && ./iterators_slicing_take_skip_step_by
[6, 7, 8, 9, 10]
[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);
}
$ rustc iterators_combining_zip_chain_enumerate.rs && ./iterators_combining_zip_chain_enumerate
[("Alice", 95), ("Bob", 87), ("Charlie", 92)]
1: Alice
2: Bob
3: Charlie
[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>());
    println!("{}", v.iter().product::<i32>());
    println!("{}", v.iter().count());
    println!("{:?}", v.iter().min());
    println!("{:?}", v.iter().max());
    println!("{}", v.iter().any(|&x| x > 3));           // stops at the first match
    println!("{}", v.iter().all(|&x| x > 0));           // stops at the first failure
    println!("{:?}", v.iter().find(|&&x| x > 3));       // the first matching element
    println!("{:?}", v.iter().position(|&x| x > 3));    // the index of the 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);

    // for_each — like a for loop but chainable in a pipeline
    v.iter().for_each(|&x| print!("{} ", x));
    println!();
}
$ rustc iterators_consuming_adapters.rs && ./iterators_consuming_adapters
15
120
5
Some(1)
Some(5)
true
true
Some(4)
Some(3)
120
1 2 3 4 5 

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);
}
$ rustc iterators_custom_iterators.rs && ./iterators_custom_iterators
(0, 0)
(1, 0)
(2, 0)
(0, 1)
(1, 1)
(2, 1)
diagonal cells: 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);

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

    // 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);
}
$ rustc iterators_std_iter_constructors.rs && ./iterators_std_iter_constructors
[3, 3, 3, 3, 3]
["hello"]
[]
[1, 4, 9, 16, 25]

Practice

  1. Write v.iter().map(|x| { println!("doubling {}", x); x * 2 }); as a statement, with nothing consuming it, then println!("done");. How many doubling lines print?

    Solution

    None — only done. The compiler warns about it:

    warning: unused `Map` that must be used
    ...
      = note: iterators are lazy and do nothing unless consumed

    Add .collect::<Vec<_>>() or a for loop and the four doubling lines appear.

  2. In the three-iteration-methods example, add println!("{:?}", v3); after the into_iter() loop. What does the compiler say, and why doesn’t the same thing happen after the iter() loop?

    Solution
    error[E0382]: borrow of moved value: `v3`
    ...
    note: `into_iter` takes ownership of the receiver `self`, which moves `v3`

    iter() takes &self and yields references, so v is only borrowed for the loop. into_iter() on a Vec takes self, so the vector is consumed.

  3. Implement Iterator for struct Countdown(u32) so that Countdown(5) yields 5, 4, 3, 2, 1. Then, without writing any more methods, compute its sum and keep only the even numbers.

    Solution
    struct Countdown(u32);
    
    impl Iterator for Countdown {
        type Item = u32;
        fn next(&mut self) -> Option<u32> {
            if self.0 == 0 {
                None
            } else {
                self.0 -= 1;
                Some(self.0 + 1)
            }
        }
    }

    Countdown(5).collect::<Vec<_>>() is [5, 4, 3, 2, 1], .sum::<u32>() is 15, and .filter(|n| n % 2 == 0) gives [4, 2] — sum and filter come free with next.

  4. For vec![2, 4, 5, 6, 7], predict take_while, filter, and skip_while with the predicate x % 2 == 0.

    Solution
    [2, 4]
    [2, 4, 6]
    [5, 6, 7]

    filter checks every element, so the 6 after the odd 5 survives. take_while stops for good at the first failure, and skip_while stops skipping there — the even 6 is kept because skipping already ended.

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.