Updated Aug 5, 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

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

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

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()
}

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

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

    // .iter() yields &i32, and .filter() passes each item by reference again,
    // giving you &&i32. Writing |&&x| destructures both layers to get the plain i32 value.
    let count = numbers.iter().filter(|&&x| x > 2).count();
    println!("count > 2: {}", count); // count > 2: 3

    // Some operators like % work on references automatically. Comparison
    // operators like > with literals need explicit * to dereference.
    // When in doubt, the compiler will tell you.
    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
}

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
  • .iter() borrows, .iter_mut() borrows mutably, .into_iter() consumes
  • 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