Iterators

Reviewed & published by Brayan K

Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and iterators let you process sequences lazily and expressively, with zero runtime cost.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll chain adapters like map and filter, finish with consumers like collect and sum, and understand laziness.

What You'll Learn in This Lesson

1️⃣ Adapters: map, filter, and collect

Call .iter() on a collection to get an iterator, then chain adapters that transform it: filter keeps matching items, map transforms each one. Adapters are lazy — they don't run until a consumer like collect or sum drives the chain.

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

    // Iterator ADAPTERS transform an iterator and are LAZY — nothing happens
    // until a CONSUMER like collect() or sum() runs the chain.
    let result: Vec<i32> = numbers
        .iter()                  // borrow each element
        .filter(|&&x| x % 2 == 0)// keep only even numbers
        .map(|&x| x * 10)        // multiply each by 10
        .collect();              // CONSUME into a new Vec

    println!("{:?}", result);    // -> [20, 40, 60]

    // sum() is a consumer that adds everything up.
    let total: i32 = numbers.iter().sum();
    println!("total: {}", total);   // -> total: 21

    // map + collect to build a new transformed vector.
    let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();
    println!("squares: {:?}", squares);   // -> [1, 4, 9, 16, 25, 36]
}

The whole filter().map() pipeline did nothing until collect() ran it. That laziness is what makes long chains efficient — no throwaway intermediate vectors.

2️⃣ Consumers: count, find, enumerate, and Ranges

Beyond collect, many consumers produce a single value: count, max, find (returns an Option). The enumerate adapter pairs each item with its index. And ranges like 1..=4 are iterators themselves.

fn main() {
    let words = vec!["apple", "banana", "cherry", "date"];

    // count, max, min, and find are consumers that produce a single value.
    let long_words = words.iter().filter(|w| w.len() > 5).count();
    println!("words longer than 5: {}", long_words);   // -> 2

    // find returns the first match as an Option.
    let first_b = words.iter().find(|w| w.starts_with('b'));
    println!("first b-word: {:?}", first_b);   // -> Some("banana")

    // enumerate pairs each item with its index.
    for (i, w) in words.iter().enumerate() {
        println!("{}: {}", i, w);
    }

    // Ranges are iterators too. Sum the squares of 1..=4.
    let sum_squares: i32 = (1..=4).map(|n| n * n).sum();
    println!("sum of squares 1..=4: {}", sum_squares);   // -> 30
}

Notice find returned an Option — there might be no match. And summing the squares of a range showed how adapters and consumers compose over anything iterable.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — finish the iterator chain, then run it.
fn main() {
    let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];

    // 1) Keep only the ODD numbers, then double them, then collect to a Vec.
    let result: Vec<i32> = nums
        .iter()
        .filter(|&&x| x % 2 == ___)    // 👉 odd numbers have remainder 1
        .map(|&x| x * 2)
        .___();                        // 👉 the consumer that builds a Vec
    println!("{:?}", result);          // expected: [2, 6, 10, 14]

    // 2) Sum all the numbers.
    let total: i32 = nums.iter().___();  // 👉 the consumer that adds them up
    println!("total = {}", total);       // expected: total = 36
}

// ✅ Expected output:
//    [2, 6, 10, 14]
//    total = 36

Mini-Challenge: Process Scores

Use adapters and consumers to count, find the max, and filter a list of scores. Run it with cargo run.

// 🎯 MINI-CHALLENGE: process a list of scores.
//
// Given let scores = vec![45, 80, 30, 95, 60, 100];
// 1. Count how many scores are passing (>= 50). Use filter + count.
// 2. Find the highest score with .iter().max() (handle the Option with
//    unwrap_or, or copied().max()).
// 3. Build a Vec of only the passing scores, each labelled by adding 0 (just
//    collect the passing ones into a Vec<i32>).
// 4. Print the count, the max, and the passing vector.
//
// ✅ Example output:
//    Passing: 4
//    Highest: 100
//    Passing scores: [80, 95, 60, 100]

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Iterators

GoalRust Syntax
Iteratev.iter()
Transform.map(|x| x * 2)
Keep some.filter(|x| cond)
Build a Vec.collect::<Vec<_>>()
Add up.sum::<i32>()
Index pairs.enumerate()

🎉 Lesson Complete!

Practice quiz

What does it mean that iterator adapters are lazy?

  • They run immediately
  • They only work on numbers
  • They do no work until a consumer drives the chain
  • They cache results

Answer: They do no work until a consumer drives the chain. Adapters build a description; nothing runs until a consumer pulls values.

Which of these is an iterator adapter?

  • map
  • collect
  • sum
  • count

Answer: map. map returns a new iterator, so it is an adapter, not a consumer.

Which of these is a consumer?

  • filter
  • map
  • enumerate
  • collect

Answer: collect. collect runs the chain to completion and builds a final collection.

What does .filter() do?

  • Transforms each item
  • Keeps only items matching a condition
  • Sums items
  • Reverses the order

Answer: Keeps only items matching a condition. filter keeps only the elements for which the closure returns true.

What does .map() do?

  • Transforms each item
  • Keeps some items
  • Counts items
  • Sorts items

Answer: Transforms each item. map applies a closure to transform every element.

What does .collect() do?

  • Adds numbers
  • Finds the max
  • Builds a collection from the iterator
  • Prints items

Answer: Builds a collection from the iterator. collect consumes the iterator into a collection like a Vec.

What does .find() return?

  • A Vec
  • An Option
  • A bool
  • The index

Answer: An Option. find returns the first matching item as an Option (Some or None).

What does .enumerate() produce?

  • Only values
  • Only indexes
  • A single count
  • Pairs of (index, item)

Answer: Pairs of (index, item). enumerate pairs each item with its position index.

How do you typically end an iterator chain?

  • With many consumers
  • With exactly one consumer
  • With no consumer
  • With a semicolon only

Answer: With exactly one consumer. A chain is several adapters followed by exactly one consumer.

How fast are idiomatic iterator chains compared to hand-written loops?

  • Much slower
  • Slightly slower always
  • Just as fast (zero-cost)
  • Only fast in release with unsafe

Answer: Just as fast (zero-cost). Iterators are a zero-cost abstraction, compiling to loop-equivalent code.

Continue this course

Frequently asked questions

What does it mean that iterators are lazy?

Iterator adapters like map and filter do no work when you call them; they just build up a description of the computation. Nothing actually runs until you call a consumer such as collect, sum, count, or a for loop, which pulls values through the whole chain. This laziness means you can chain many transformations efficiently without creating intermediate collections at each step, and you can even work with infinite iterators as long as something eventually stops pulling.

What is the difference between an adapter and a consumer?

An iterator adapter takes an iterator and returns a new iterator, transforming the sequence — examples include map, filter, take, skip, and enumerate. Because they return iterators, adapters can be chained together. A consumer (also called a consuming adapter) is a method that runs the iterator to completion and produces a final value or collection — examples include collect, sum, count, max, find, and for_each. A typical chain is several adapters followed by exactly one consumer at the end.

What is the difference between iter, iter_mut, and into_iter?

They differ in how they yield elements. iter() yields shared references (&T), letting you read elements without taking ownership. iter_mut() yields mutable references (&mut T), letting you modify elements in place. into_iter() consumes the collection and yields owned values (T), which is useful when you no longer need the original. Choosing the right one depends on whether you want to read, modify, or take ownership of the elements as you iterate.

Why use iterators instead of a plain for loop with an index?

Iterators are more concise, more expressive, and safer. They eliminate manual index bookkeeping, so you cannot go out of bounds or make off-by-one mistakes. Chains of adapters read almost like a description of the transformation you want. And thanks to Rust's zero-cost abstractions, idiomatic iterator chains compile down to code that is just as fast as a hand-written loop — often the compiler produces identical machine code. For these reasons, iterators are the idiomatic way to process sequences in Rust.