Updated Aug 6, 2026

20 - Iterators

📋 Jump to Takeaways

🎁 What if you could describe a data transformation pipeline, filter this, transform that, take the first five, and the compiler would fuse it all into a single tight loop with no temporary allocations? No intermediate vectors, no heap overhead, just one pass through the data.

The Iterator Trait

An iterator is a value that produces elements one at a time. You call .next() to get the next element, or you chain adaptors to describe a transformation, then call a consumer to execute it. Nothing runs until you consume.

The Iterator trait has one required method: next(), which returns Option<Self::Item> (Item is the type of element this iterator produces, each iterator defines this).

fn main() {
    let nums = vec![10, 20, 30];
    let mut iter = nums.iter(); // mut because .next() advances the iterator's internal position

    println!("{:?}", iter.next()); // Some(10)
    println!("{:?}", iter.next()); // Some(20)
    println!("{:?}", iter.next()); // Some(30)
    println!("{:?}", iter.next()); // None
}

You can also drive an iterator manually with while let, which is exactly what a for loop desugars to:

fn main() {
    let nums = vec![10, 20, 30];
    let mut iter = nums.iter();

    while let Some(a) = iter.next() {
        println!("{:?}", a); // 10, 20, 30
    }
}

With a for loop you don't need mut — the for loop takes ownership of the iterator and mutates it internally, so you never touch it directly:

fn main() {
    let nums = vec![10, 20, 30];
    let iter = nums.iter(); // no mut needed

    for a in iter {
        println!("{:?}", a); // 10, 20, 30
    }
}

When you call .next() yourself you hold the iterator, so you need mut. When for calls .next(), it holds the iterator and handles the mutation for you.

After the for loop, iter is consumed and gone. But the original collection is still usable because .iter() only borrowed it:

fn main() {
    let nums = vec![10, 20, 30];
    let iter = nums.iter();

    for a in iter {
        println!("{:?}", a);
    }

    // println!("{:?}", iter); // ❌ iter was consumed by the for loop
    println!("{:?}", nums);    // ✅ nums is still valid — only the iterator was consumed
}

Iterators are lazy, they do nothing until you consume them. This is what enables the compiler to fuse the entire chain into a single loop.

Creating Iterators

You have three ways to create an iterator from a collection:

  • .iter(), iterates over &T (immutable references)
  • .iter_mut(), iterates over &mut T (mutable references)
  • .into_iter(), iterates over T (takes ownership)
fn main() {
    let mut names = vec!["Alice", "Bob", "Carol"];

    // .iter() — borrows, names stays usable
    for name in names.iter() {
        println!("{}", name);
    }

    // .iter_mut() — borrows mutably, can modify in place
    for name in names.iter_mut() {
        *name = "Modified";
    }
    println!("{:?}", names); // ["Modified", "Modified", "Modified"] — still usable

    // .into_iter() — moves each element out, names is gone after this
    for name in names.into_iter() {
        println!("got ownership of: {}", name);
    }
    // println!("{:?}", names); // ERROR — names was consumed by into_iter()
}

A for loop is just syntactic sugar for .into_iter(). These two are identical:

let names = vec!["Alice", "Bob"];

// sugar
for name in names {
    println!("{}", name);
}

// what Rust actually does
for name in names.into_iter() {
    println!("{}", name);
}

When you write for x in collection, Rust calls .into_iter() on the collection and drives it with .next() under the hood. That's why the collection is consumed after a for loop — same as .into_iter().

Iterator Adaptors

Adaptors transform an iterator into another iterator. They're lazy — nothing happens until you consume the result. Think of them as building a pipeline. No work runs yet.

Adaptors (lazy, return another iterator — chain as many as you want): .map(), .filter(), .enumerate(), .zip(), .skip(), .take(), .chain()

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

    // .map — transform each element
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("{:?}", doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

    // .filter — keep elements matching a predicate
    let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
    println!("{:?}", evens); // [2, 4, 6, 8, 10]

    // .enumerate — attach index to each element
    for (i, val) in numbers.iter().enumerate().take(3) {
        println!("index {}: {}", i, val); // index 0: 1, index 1: 2, index 2: 3
    }

    // .zip — pair elements from two iterators
    let letters = vec!['a', 'b', 'c'];
    // The _ tells Rust to infer the element type — you're specifying
    // it's a Vec but letting the compiler figure out what's inside
    let zipped: Vec<_> = numbers.iter().zip(letters.iter()).collect();
    println!("{:?}", zipped); // [(1, 'a'), (2, 'b'), (3, 'c')]

    // .skip and .chain
    let skipped: Vec<&i32> = numbers.iter().skip(7).collect();
    println!("{:?}", skipped); // [8, 9, 10]

    let extra = vec![11, 12];
    let chained: Vec<&i32> = numbers.iter().chain(extra.iter()).skip(8).collect();
    println!("{:?}", chained); // [9, 10, 11, 12]
}

The Double Reference Problem

.iter() yields &T (references). When you pass that into .filter(), the closure receives each element by reference again — so you get &&T, a reference to a reference. This catches almost every Rust beginner.

let numbers = vec![1, 2, 3];

numbers.iter().filter(|x| x > 2);    // ❌ x is &&i32, comparing &&i32 > i32 fails
numbers.iter().filter(|x| *x > 2);   // ✅ one dereference: &i32, Rust auto-derefs the rest
numbers.iter().filter(|x| **x > 2);  // ✅ two explicit dereferences: plain i32
numbers.iter().filter(|&&x| x > 2);  // ✅ destructure both layers in the pattern

One * is often enough. *x takes you from &&i32 to &i32, and Rust auto-derefs &i32 when applying operators like >, %, ==. You only need **x or |&&x| when auto-deref doesn't kick in or you want to be explicit.

The pattern |&&x| in the closure parameters is destructuring: the outer & unwraps what filter adds, the inner & unwraps what .iter() added, leaving x as a plain i32.

.map() doesn't have this issue because it receives one reference level (&T), not two. .filter() is the main place you'll see &&.

Consumers

Consumers drive the iterator and produce a final value. They pull every element through the pipeline and collapse it into a result. Once consumed, the iterator is gone.

Consumers (eager, end the chain and return a value — pick one): .collect(), .sum(), .product(), .count(), .any(), .all(), .find(), .position(), .fold()

Consumers are methods on the Iterator trait, not on Vec or other collections directly. You always need .iter(), .iter_mut(), or .into_iter() first:

let total: i32 = numbers.sum();        // ❌ ERROR — Vec doesn't have .sum()
let total: i32 = numbers.iter().sum(); // ✅
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    let total: i32 = numbers.iter().sum();
    println!("sum: {}", total); // sum: 15

    // .filter() receives &&i32 — .iter() gives &i32, filter adds another &.
    // |&&x| destructures both layers, giving a plain i32.
    // |x| *x > 2 also works — one * gets to &i32, Rust auto-derefs the rest.
    let count = numbers.iter().filter(|&&x| x > 2).count();
    println!("count > 2: {}", count); // count > 2: 3

    let has_even = numbers.iter().any(|x| x % 2 == 0);
    println!("has even: {}", has_even); // has even: true

    let all_positive = numbers.iter().all(|x| *x > 0);
    println!("all positive: {}", all_positive); // all positive: true

    // .find — returns the first element matching a condition, or None
    let first_big = numbers.iter().find(|x| **x > 3);
    println!("first > 3: {:?}", first_big); // first > 3: Some(4)

    // .fold — like .sum() but you control how elements combine.
    // First argument is the starting value, closure gets (accumulated, current).
    let product = numbers.iter().fold(1, |acc, x| acc * x);
    // step by step: 1*1=1, 1*2=2, 2*3=6, 6*4=24, 24*5=120
    println!("product: {}", product); // product: 120
}

Map, Filter, Reduce

Rust's iterator system is the same concept as map/filter/reduce from JavaScript and functional programming — just with different names:

Concept Rust JavaScript
Transform each element .map() .map()
Keep matching elements .filter() .filter()
Collapse to one value .fold() .reduce()
Transform + flatten .flat_map() .flatMap()

.fold() is Rust's reduce. It's more explicit — you provide the starting value and the combining logic:

let sum = vec![1, 2, 3, 4, 5]
    .iter()
    .fold(0, |acc, x| acc + x); // 15 — same as .sum()

// fold into anything, not just numbers
let sentence = vec!["hello", "world"]
    .iter()
    .fold(String::new(), |acc, w| acc + w + " "); // "hello world "

The key difference from JavaScript is that Rust's pipeline is lazy and zero-cost. In JS, .filter().map().reduce() creates a new array after each step. In Rust the whole chain compiles to one loop with no intermediate allocations.

Method Chaining

The real power emerges when you chain multiple adaptors together. Each step is clear, composable, and zero-cost.

fn main() {
    let words = vec!["hello", "world", "rust", "is", "fast"];

    let result: String = words
        .iter()
        .filter(|w| w.len() > 3)
        .map(|w| w.to_uppercase())
        .collect::<Vec<String>>()
        .join(", ");

    println!("{}", result); // HELLO, WORLD, RUST, FAST
}

.collect() needs to know what collection type to build. You have two ways to tell it:

// Option 1: annotate the variable — collect() figures it out
let v: Vec<String> = words.iter().map(|w| w.to_uppercase()).collect();

// Option 2: turbofish — put the type on collect() itself
let v = words.iter().map(|w| w.to_uppercase()).collect::<Vec<String>>();

Both do the same thing. Turbofish (::<>) is useful when you can't annotate the variable, like when returning a value inline from a function.

You can read this top to bottom: take the words, keep those longer than 3 characters, uppercase them, collect into a vector, join with commas.

Zero-Cost Abstraction

When you write .filter().map().sum(), you might expect Rust to build a temporary vector after each step. It doesn't. The compiler sees the whole chain at once, inlines each closure, and merges everything into a single loop — the same machine code you'd write by hand.

In most languages, chaining methods means paying for the convenience: extra allocations, virtual function calls, or multiple passes over the data. In Rust, you pay nothing. That's what "zero-cost abstraction" means: the readable version and the hand-optimized version are identical after compilation.

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

    // This iterator chain...
    let sum_a: i32 = numbers.iter().filter(|&&x| x % 2 == 0).map(|x| x * x).sum();

    // ...compiles to the same machine code as this loop:
    // &numbers is shorthand for .iter(), and &x in the pattern destructures
    // the reference so x is the value directly.
    let mut sum_b = 0;
    for &x in &numbers {
        if x % 2 == 0 {
            sum_b += x * x;
        }
    }

    println!("{} == {}", sum_a, sum_b); // 220 == 220
}

The iterator version is more readable, more composable, and equally fast. This is what "zero-cost abstraction" means in Rust.

Key Takeaways

  • Iterators are lazy, adaptors build a pipeline, consumers execute it
  • for x in collection is sugar for .into_iter() — the collection is consumed after the loop
  • .iter() borrows, .iter_mut() borrows mutably, .into_iter() consumes
  • .filter() passes elements by reference again, giving &&T — use |&&x| or **x to unwrap
  • Chain adaptors like .map(), .filter(), .enumerate(), .zip() freely
  • Consumers like .collect(), .sum(), .fold(), .find() drive execution
  • .collect() needs a type hint (annotation or turbofish) to know what to build
  • Iterator chains compile to the same machine code as manual loops, zero-cost abstraction
  • No intermediate allocations between adaptor steps, the compiler fuses the chain

🎁 You've called println!, vec!, and assert! throughout this course — all with a ! suffix. That's not decoration. Next up: what macros actually are, why they exist, and how to write your own.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy