Skip to editor content
learningrust.orglesson 15 of 26

Iterators

Iterators are one of Rust's most powerful and elegant features. They let you process sequences of elements in a composable, zero-cost way — meaning the compiler optimizes iterator chains down to the same machine code as hand-written loops.

What Is an Iterator?

An iterator is any type that implements the Iterator trait, which requires a single method: next(). Each call to next() returns Some(item) until the sequence is exhausted, then returns None.

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

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

In practice, you rarely call next() manually. Rust's for loop and iterator adapters handle that for you.

Creating Iterators

There are three common ways to get an iterator from a collection:

  • .iter() — borrows each element as &T
  • .iter_mut() — borrows each element as &mut T
  • .into_iter() — consumes the collection, yielding T
fn main() {
    let fruits = vec!["apple", "banana", "cherry"];

    // Borrow — fruits is still usable after the loop
    for fruit in fruits.iter() {
        println!("Borrowed: {}", fruit);
    }

    // into_iter on a reference gives &T too (common in for loops)
    for fruit in &fruits {
        println!("Via reference: {}", fruit);
    }

    println!("fruits still alive: {:?}", fruits);
}

Iterator Adapters

Iterator adapters are methods that transform one iterator into another. They are lazy — nothing runs until you consume the iterator.

map — Transform Each Element

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

    let doubled: Vec<i32> = numbers
        .iter()
        .map(|&n| n * 2)
        .collect();

    println!("Original: {:?}", numbers);
    println!("Doubled:  {:?}", doubled);

    // Chain multiple adapters
    let squared_evens: Vec<i32> = numbers
        .iter()
        .filter(|&&n| n % 2 == 0)
        .map(|&n| n * n)
        .collect();

    println!("Squared evens: {:?}", squared_evens);
}

filter — Keep Only Matching Elements

filter takes a closure that returns true to keep an element or false to skip it.

enumerate — Track Position

enumerate wraps each element with its index, yielding (index, item) pairs.

fn main() {
    let tasks = vec!["write tests", "fix bug", "deploy", "review PR"];

    for (i, task) in tasks.iter().enumerate() {
        println!("Task {}: {}", i + 1, task);
    }

    // Find the index of the first task containing "bug"
    let bug_task = tasks
        .iter()
        .enumerate()
        .find(|(_, task)| task.contains("bug"));

    match bug_task {
        Some((i, task)) => println!("Found at position {}: {}", i, task),
        None => println!("No bug tasks found"),
    }
}

Consuming Adapters

Consuming adapters call next() and exhaust the iterator. Common ones include:

MethodPurpose
.collect()Gather into a collection
.sum()Add all elements
.count()Count elements
.any()True if any element matches
.all()True if all elements match
.find()First matching element
.fold()Reduce with an accumulator
fn main() {
    let scores = vec![85, 92, 78, 95, 88, 60, 73];

    let total: i32 = scores.iter().sum();
    let count = scores.len();
    let average = total / count as i32;

    let passing: Vec<&i32> = scores.iter().filter(|&&s| s >= 70).collect();
    let all_pass = scores.iter().all(|&s| s >= 50);
    let has_distinction = scores.iter().any(|&s| s >= 90);

    println!("Total: {}, Average: {}", total, average);
    println!("Passing scores: {:?}", passing);
    println!("All above 50: {}", all_pass);
    println!("Has distinction (>=90): {}", has_distinction);

    // fold: compute product of all scores (just for demonstration)
    let max_score = scores.iter().fold(i32::MIN, |acc, &s| acc.max(s));
    println!("Highest score: {}", max_score);
}

Implementing Your Own Iterator

You can make any struct iterable by implementing Iterator:

struct Countdown {
    count: u32,
}

impl Countdown {
    fn new(start: u32) -> Self {
        Countdown { count: start }
    }
}

impl Iterator for Countdown {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count == 0 {
            None
        } else {
            let current = self.count;
            self.count -= 1;
            Some(current)
        }
    }
}

fn main() {
    let countdown = Countdown::new(5);

    // All iterator methods work for free!
    let values: Vec<u32> = countdown.collect();
    println!("Countdown: {:?}", values);

    // Use adapters on your custom iterator
    let even_counts: Vec<u32> = Countdown::new(10)
        .filter(|n| n % 2 == 0)
        .collect();
    println!("Even counts: {:?}", even_counts);
}

Because Countdown implements Iterator, it automatically gains access to all iterator adapter methods like map, filter, take, zip, and dozens more.

Chaining Iterators

Two useful adapters for combining iterators:

  • .chain() — concatenate two iterators
  • .zip() — pair elements from two iterators
fn main() {
    let first = vec![1, 2, 3];
    let second = vec![4, 5, 6];

    // chain: iterate both sequences as one
    let combined: Vec<i32> = first.iter().chain(second.iter()).copied().collect();
    println!("Chained: {:?}", combined);

    // zip: pair elements together
    let names = vec!["Alice", "Bob", "Carol"];
    let scores = vec![95, 87, 92];

    let leaderboard: Vec<(&str, i32)> = names
        .iter()
        .copied()
        .zip(scores.iter().copied())
        .collect();

    for (name, score) in &leaderboard {
        println!("{}: {}", name, score);
    }
}

Try It Yourself

Use iterators to process a list of words. Find all words longer than 4 characters, convert them to uppercase, and sort them alphabetically:

fn main() {
    let words = vec![
        "rust", "iterator", "closure", "map", "filter",
        "collect", "trait", "impl", "enum", "match",
    ];

    let mut result: Vec<String> = words
        .iter()
        .filter(|w| w.len() > 4)
        .map(|w| w.to_uppercase())
        .collect();

    result.sort();

    println!("Long words (uppercase, sorted):");
    for word in &result {
        println!("  {}", word);
    }

    println!("\nTotal: {} words", result.len());
}

Try modifying this to also include the word length in the output, or filter by a different condition.

Key Takeaways

  • Iterators implement the Iterator trait with a single next() method that returns Option<Item>
  • Use .iter() for borrows, .iter_mut() for mutable borrows, and .into_iter() to consume a collection
  • Iterator adapters like map, filter, enumerate, and zip are lazy — they do no work until consumed
  • Consuming adapters like collect, sum, fold, and any drive the iteration to completion
  • Implementing Iterator on your own types gives you all adapter methods for free
  • Iterator chains compile to efficient machine code with zero runtime overhead — they are not slower than manual loops
  • Prefer iterator chains over manual loops when the intent is clearer and the logic is functional in nature

Pro Tip: When you need both the index and the value in a loop, reach for .enumerate() instead of maintaining a manual counter. And when your iterator chain gets hard to read, consider breaking it into named intermediate variables — Rust's type inference handles the types, so you only need to name the values.

Next Steps

Now that you're comfortable with iterators, we'll explore strings and text processing — a topic where Rust's ownership model has some unique implications.

Next lesson

Strings and Text

Understand Rust's two string types, text manipulation, UTF-8 encoding, and common string operations

25 min