Skip to main content

Iterators in Rust

Published: October 28, 2025 Updated: August 28, 2026 Larry Qu 9 min read

Rust’s iterator system is one of its most powerful features. Iterators are lazy, composable, and compile to code that is just as fast as hand-written loops — often faster, because the compiler can better reason about the intent. Everything from standard library collections to custom data structures participates in the same unified Iterator trait.

The Iterator Trait

All iterators implement a single trait with one required method:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 70+ default methods built on top of next()
}

When next() returns Some(item), iteration continues. When it returns None, iteration ends. The for loop is syntactic sugar for calling next() in a loop:

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

    // These two are identical:
    for x in &v {
        println!("{}", x);
    }

    let mut iter = v.iter();
    while let Some(x) = iter.next() {
        println!("{}", x);
    }
}

The Three Iterator Methods on Collections

Every collection gives you three ways to get an iterator, with different ownership semantics:

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

    // iter() — borrows immutably, yields &T
    for word in words.iter() {
        println!("{}", word); // word: &&str
    }
    println!("words still available: {:?}", words);

    // iter_mut() — borrows mutably, yields &mut T
    let mut numbers = vec![1, 2, 3, 4, 5];
    for n in numbers.iter_mut() {
        *n *= 10;
    }
    println!("{:?}", numbers); // [10, 20, 30, 40, 50]

    // into_iter() — takes ownership, yields T
    let owned = vec![String::from("a"), String::from("b")];
    for s in owned.into_iter() {
        println!("Owned: {}", s);
    }
    // `owned` is consumed — no longer accessible
}

Using for x in collection without calling a method calls into_iter() implicitly.

Iterator Adapters

Adapters transform one iterator into another. They are lazy — no work happens until a consuming method is called. This lets you build complex pipelines with zero intermediate allocations.

map — Transform Each Element

fn main() {
    let prices = vec![9.99, 14.99, 4.99];

    let with_tax: Vec<f64> = prices.iter()
        .map(|&p| (p * 1.08 * 100.0).round() / 100.0)
        .collect();

    println!("{:?}", with_tax); // [10.79, 16.19, 5.39]
}

filter — Keep Matching Elements

fn main() {
    let users = vec![
        ("alice", true),
        ("bob", false),
        ("carol", true),
    ];

    let active: Vec<&str> = users.iter()
        .filter(|(_, active)| *active)
        .map(|(name, _)| *name)
        .collect();

    println!("{:?}", active); // ["alice", "carol"]
}

filter_map — Filter and Transform in One Step

Avoids a nested filter + map when the transform can fail:

fn main() {
    let raw = vec!["42", "not_a_number", "17", "", "99"];

    let numbers: Vec<i32> = raw.iter()
        .filter_map(|s| s.parse().ok())
        .collect();

    println!("{:?}", numbers); // [42, 17, 99]
}

flat_map — Map and Flatten

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

    let words: Vec<&str> = sentences.iter()
        .flat_map(|s| s.split_whitespace())
        .collect();

    println!("{:?}", words); // ["hello", "world", "rust", "is", "great"]
}

enumerate — Index + Value

fn main() {
    let fruits = ["apple", "banana", "cherry"];

    for (i, fruit) in fruits.iter().enumerate() {
        println!("{}: {}", i, fruit);
    }
    // 0: apple, 1: banana, 2: cherry
}

zip — Pair Two Iterators

fn main() {
    let names = ["Alice", "Bob", "Carol"];
    let scores = [92, 88, 95];

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

    println!("{:?}", leaderboard);
    // [("Alice", 92), ("Bob", 88), ("Carol", 95)]
}

take and skip

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

    // Pagination: page 2, page size 3
    let page_size = 3;
    let page = 1; // 0-indexed
    let page_items: Vec<_> = data.iter()
        .skip(page * page_size)
        .take(page_size)
        .collect();

    println!("{:?}", page_items); // [4, 5, 6]
}

chain — Concatenate Two Iterators

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

    let combined: Vec<i32> = first.iter()
        .chain(second.iter())
        .copied()
        .collect();

    println!("{:?}", combined); // [1, 2, 3, 4, 5, 6]
}

peekable — Look Ahead Without Consuming

fn main() {
    let mut iter = vec![1, 2, 3].into_iter().peekable();

    while let Some(&next) = iter.peek() {
        if next > 2 {
            break;
        }
        println!("{}", iter.next().unwrap());
    }
    // Prints 1, 2
}

windows and chunks — Slice-Based Iteration

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

    // Sliding window of size 3
    for window in data.windows(3) {
        let sum: i32 = window.iter().sum();
        println!("{:?} → sum {}", window, sum);
    }
    // [1, 2, 3] → sum 6
    // [2, 3, 4] → sum 9
    // [3, 4, 5] → sum 12

    // Non-overlapping chunks
    for chunk in data.chunks(2) {
        println!("{:?}", chunk);
    }
    // [1, 2], [3, 4], [5]
}

Consuming Adaptors

These methods drive the iterator to completion and produce a final value.

collect

The most versatile consumer — turns an iterator into any collection:

use std::collections::{HashMap, HashSet};

fn main() {
    let pairs = vec![("a", 1), ("b", 2), ("c", 3)];

    // Into HashMap
    let map: HashMap<&str, i32> = pairs.into_iter().collect();
    println!("{:?}", map);

    // Into HashSet (deduplication)
    let dupes = vec![1, 2, 2, 3, 3, 3];
    let unique: HashSet<i32> = dupes.into_iter().collect();
    println!("{:?}", unique); // {1, 2, 3}

    // Join strings
    let words = vec!["one", "two", "three"];
    let joined: String = words.join(", ");
    println!("{}", joined); // one, two, three
}

fold and reduce

fold accumulates a result with an initial value; reduce uses the first element as the initial value:

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

    // fold: build a product
    let product = numbers.iter().fold(1i64, |acc, &x| acc * x as i64);
    println!("{}", product); // 120

    // reduce: max value
    let max = numbers.iter().copied().reduce(|a, b| if a > b { a } else { b });
    println!("{:?}", max); // Some(5)

    // Building a string with fold
    let csv = numbers.iter()
        .fold(String::new(), |mut acc, &n| {
            if !acc.is_empty() { acc.push(','); }
            acc.push_str(&n.to_string());
            acc
        });
    println!("{}", csv); // 1,2,3,4,5
}

any and all

Short-circuit boolean checks:

fn main() {
    let values = vec![2, 4, 6, 7, 8];

    println!("{}", values.iter().all(|&x| x % 2 == 0));  // false (7 is odd)
    println!("{}", values.iter().any(|&x| x % 2 != 0));  // true (7)

    // Useful for validation
    let emails = vec!["[email protected]", "invalid", "[email protected]"];
    let all_valid = emails.iter().all(|e| e.contains('@'));
    println!("All valid: {}", all_valid); // false
}

find and position

fn main() {
    let users = vec![
        (1u32, "Alice"),
        (2, "Bob"),
        (3, "Carol"),
    ];

    let found = users.iter().find(|(id, _)| *id == 2);
    println!("{:?}", found); // Some((2, "Bob"))

    let pos = users.iter().position(|(_, name)| *name == "Carol");
    println!("{:?}", pos); // Some(2)
}

sum and product

fn main() {
    let v = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
    let sum: f64 = v.iter().sum();
    let product: f64 = v.iter().product();
    println!("sum={}, product={}", sum, product); // sum=15, product=120
}

Implementing a Custom Iterator

Implement Iterator for any type by providing next(). All 70+ standard adapters become available automatically.

A Range-Step Iterator

struct StepRange {
    current: i32,
    end: i32,
    step: i32,
}

impl StepRange {
    fn new(start: i32, end: i32, step: i32) -> Self {
        StepRange { current: start, end, step }
    }
}

impl Iterator for StepRange {
    type Item = i32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.end {
            let val = self.current;
            self.current += self.step;
            Some(val)
        } else {
            None
        }
    }
}

fn main() {
    let evens: Vec<i32> = StepRange::new(0, 20, 2).collect();
    println!("{:?}", evens); // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

    // Free adapters from implementing Iterator
    let sum: i32 = StepRange::new(1, 10, 2).sum();
    println!("Sum of odds 1-9: {}", sum); // 25
}

A Tree Node Iterator (Depth-First)

#[derive(Debug)]
struct TreeNode {
    value: i32,
    children: Vec<TreeNode>,
}

struct DfsIterator {
    stack: Vec<TreeNode>,
}

impl DfsIterator {
    fn new(root: TreeNode) -> Self {
        DfsIterator { stack: vec![root] }
    }
}

impl Iterator for DfsIterator {
    type Item = i32;

    fn next(&mut self) -> Option<Self::Item> {
        let node = self.stack.pop()?;
        // Push children in reverse order for correct DFS order
        for child in node.children.into_iter().rev() {
            self.stack.push(child);
        }
        Some(node.value)
    }
}

fn main() {
    let tree = TreeNode {
        value: 1,
        children: vec![
            TreeNode { value: 2, children: vec![
                TreeNode { value: 4, children: vec![] },
                TreeNode { value: 5, children: vec![] },
            ]},
            TreeNode { value: 3, children: vec![
                TreeNode { value: 6, children: vec![] },
            ]},
        ],
    };

    let values: Vec<i32> = DfsIterator::new(tree).collect();
    println!("{:?}", values); // [1, 2, 4, 5, 3, 6]
}

Performance: Zero-Cost Abstraction

Iterator chains compile down to the same machine code as hand-written loops. The Rust compiler aggressively inlines and unrolls iterator chains. In many cases iterators are faster than loops because they express intent clearly, enabling better auto-vectorization.

fn sum_of_squares_loop(data: &[f64]) -> f64 {
    let mut sum = 0.0;
    for &x in data {
        sum += x * x;
    }
    sum
}

fn sum_of_squares_iter(data: &[f64]) -> f64 {
    data.iter().map(|&x| x * x).sum()
}

// Both compile to identical (or near-identical) machine code.
// The iterator version often enables SIMD auto-vectorization.

Parallel Iteration with Rayon

For CPU-bound work, the rayon crate adds a par_iter() method that distributes work across threads automatically — no manual thread management:

// Cargo.toml: rayon = "1.10"
use rayon::prelude::*;

fn main() {
    let data: Vec<u64> = (0..1_000_000).collect();

    // Parallel sum — uses all CPU cores automatically
    let sum: u64 = data.par_iter().sum();
    println!("{}", sum);

    // Parallel filter + map
    let result: Vec<u64> = data.par_iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .collect();

    println!("Even squares count: {}", result.len());
}

The API is identical to the sequential iterator API — just change .iter() to .par_iter().

Common Patterns

Grouping with HashMap

use std::collections::HashMap;

fn main() {
    let words = vec!["apple", "ant", "bear", "avocado", "bat", "cherry"];

    let grouped: HashMap<char, Vec<&str>> = words.iter()
        .fold(HashMap::new(), |mut map, &word| {
            map.entry(word.chars().next().unwrap())
               .or_default()
               .push(word);
            map
        });

    for (letter, words) in &grouped {
        println!("{}: {:?}", letter, words);
    }
}

Early Exit with try_fold

When your fold can fail, use try_fold:

fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
    inputs.iter().try_fold(Vec::new(), |mut acc, s| {
        acc.push(s.parse::<i32>()?);
        Ok(acc)
    })
}

fn main() {
    println!("{:?}", parse_all(&["1", "2", "3"]));     // Ok([1, 2, 3])
    println!("{:?}", parse_all(&["1", "bad", "3"]));   // Err(...)
}

Summary

Method Type Description
map Adapter Transform each element
filter Adapter Keep elements matching predicate
filter_map Adapter Transform, drop None results
flat_map Adapter Map + flatten one level
enumerate Adapter Pair with index
zip Adapter Pair two iterators
take / skip Adapter Limit or offset iteration
chain Adapter Concatenate two iterators
peekable Adapter Non-consuming lookahead
collect Consumer Gather into a collection
fold Consumer Reduce with accumulator
any / all Consumer Short-circuit boolean check
find / position Consumer Search
sum / product Consumer Aggregate numerics

The key insight: iterators express what you want to compute, not how. The compiler figures out the most efficient implementation, and you get readable, composable code with no overhead.

Resources

Comments

👍 Was this article helpful?