2 min read
On this page

Iterators

Iterators are Rust's abstraction for sequential processing. They are lazy, composable, and zero-cost — iterator chains compile down to the same machine code as hand-written loops. Once you internalize the iterator API, you will write less code, produce fewer bugs, and often get better performance than manual loops.

The Three Iterator Types

Every collection in Rust can produce three kinds of iterators. Understanding the difference is essential:

fn main() {
    let names = vec![
        String::from("Alice"),
        String::from("Bob"),
        String::from("Charlie"),
    ];

    // .iter() — borrows each element as &T
    for name in names.iter() {
        println!("Hello, {}", name); // name is &String
    }
    // names is still valid here

    // .iter_mut() — borrows each element as &mut T
    let mut numbers = vec![1, 2, 3, 4, 5];
    for n in numbers.iter_mut() {
        *n *= 2; // modify in place
    }
    println!("Doubled: {:?}", numbers);

    // .into_iter() — takes ownership, consumes the collection
    let names2 = vec![
        String::from("Dave"),
        String::from("Eve"),
    ];
    for name in names2.into_iter() {
        println!("Consumed: {}", name); // name is String (owned)
    }
    // names2 is no longer valid — it was consumed
}
Hello, Alice
Hello, Bob
Hello, Charlie
Doubled: [2, 4, 6, 8, 10]
Consumed: Dave
Consumed: Eve

The for loop calls .into_iter() by default. Writing for x in &collection calls .iter(), and for x in &mut collection calls .iter_mut().

map, filter, fold

These are the building blocks of iterator processing:

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

    // map: transform each element
    let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
    println!("Squares: {:?}", squares);

    // filter: keep elements matching a predicate
    let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
    println!("Evens: {:?}", evens);

    // fold: accumulate into a single value
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Sum: {}", sum);

    // Chaining: filter then map
    let even_squares: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .collect();
    println!("Even squares: {:?}", even_squares);
}
Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Evens: [2, 4, 6, 8, 10]
Sum: 55
Even squares: [4, 16, 36, 64, 100]

Iterator Chains Are Zero-Cost

This is the critical insight. Iterator chains do not create intermediate collections. They compile to the same code as a manual loop:

fn manual_loop(data: &[i32]) -> i32 {
    let mut sum = 0;
    for &x in data {
        if x > 0 {
            sum += x * x;
        }
    }
    sum
}

fn iterator_chain(data: &[i32]) -> i32 {
    data.iter()
        .filter(|&&x| x > 0)
        .map(|&x| x * x)
        .sum()
}

fn main() {
    let data = vec![-3, -1, 0, 2, 4, -5, 7];
    println!("Manual: {}", manual_loop(&data));
    println!("Iterator: {}", iterator_chain(&data));
}
Manual: 69
Iterator: 69

In release mode, the compiler optimizes both functions to identical machine code. The iterator version is often preferred because it is more declarative and harder to mess up.

Lazy Evaluation

Iterators are lazy. Nothing happens until you consume them:

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

    // This does nothing — the iterator is created but never consumed
    let _lazy = numbers.iter().map(|x| {
        println!("Processing {}", x); // never printed
        x * 2
    });

    println!("Before collect:");

    // Now it runs — collect() consumes the iterator
    let doubled: Vec<i32> = numbers.iter().map(|&x| {
        println!("Processing {}", x);
        x * 2
    }).collect();

    println!("Result: {:?}", doubled);
}
Before collect:
Processing 1
Processing 2
Processing 3
Processing 4
Processing 5
Result: [2, 4, 6, 8, 10]

Laziness means you can build complex pipelines without wasting memory on intermediate results.

Essential Iterator Methods

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

    // any / all — short-circuit boolean checks
    let has_negative = data.iter().any(|&x| x < 0);
    let all_positive = data.iter().all(|&x| x > 0);
    println!("Has negative: {}, All positive: {}", has_negative, all_positive);

    // find — first matching element
    let first_big = data.iter().find(|&&x| x > 5);
    println!("First > 5: {:?}", first_big);

    // position — index of first match
    let pos = data.iter().position(|&x| x == 9);
    println!("Position of 9: {:?}", pos);

    // take / skip — slice the iterator
    let first_three: Vec<_> = data.iter().take(3).collect();
    let after_five: Vec<_> = data.iter().skip(5).collect();
    println!("First 3: {:?}", first_three);
    println!("After 5: {:?}", after_five);

    // enumerate — index + value pairs
    for (i, val) in data.iter().enumerate().take(4) {
        println!("  [{}] = {}", i, val);
    }

    // zip — pair elements from two iterators
    let keys = vec!["a", "b", "c"];
    let vals = vec![1, 2, 3];
    let pairs: Vec<_> = keys.iter().zip(vals.iter()).collect();
    println!("Pairs: {:?}", pairs);

    // chain — concatenate iterators
    let first = vec![1, 2];
    let second = vec![3, 4];
    let combined: Vec<_> = first.iter().chain(second.iter()).collect();
    println!("Combined: {:?}", combined);

    // flat_map — map then flatten
    let sentences = vec!["hello world", "foo bar baz"];
    let words: Vec<&str> = sentences.iter().flat_map(|s| s.split_whitespace()).collect();
    println!("Words: {:?}", words);
}
Has negative: false, All positive: true
First > 5: Some(9)
Position of 9: Some(5)
First 3: [3, 1, 4]
After 5: [9, 2, 6, 5, 3, 5]
  [0] = 3
  [1] = 1
  [2] = 4
  [3] = 1
Pairs: [("a", 1), ("b", 2), ("c", 3)]
Combined: [1, 2, 3, 4]
Words: ["hello", "world", "foo", "bar", "baz"]

collect: The Universal Consumer

collect transforms an iterator into any collection that implements FromIterator:

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

fn main() {
    let data = vec![("alice", 95), ("bob", 87), ("charlie", 92), ("alice", 88)];

    // Into a HashMap (last value wins for duplicate keys)
    let scores: HashMap<&str, i32> = data.iter().cloned().collect();
    println!("Scores: {:?}", scores);

    // Into a HashSet (unique values only)
    let names: HashSet<&str> = data.iter().map(|(name, _)| *name).collect();
    println!("Names: {:?}", names);

    // Into a BTreeMap (sorted)
    let sorted: BTreeMap<&str, i32> = data.iter().cloned().collect();
    println!("Sorted: {:?}", sorted);

    // Collect Results — stops at first error
    let numbers: Result<Vec<i32>, _> = vec!["1", "2", "three", "4"]
        .iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Parsed: {:?}", numbers);

    // String from chars
    let greeting: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
    println!("Uppercased: {}", greeting);
}
Scores: {"bob": 87, "alice": 88, "charlie": 92}
Names: {"charlie", "alice", "bob"}
Sorted: {"alice": 88, "bob": 87, "charlie": 92}
Parsed: Err(invalid digit found in string)
Uppercased: HELLO

Real-World Example: Log Processing

#[derive(Debug)]
struct LogEntry {
    level: String,
    message: String,
    timestamp: u64,
}

fn parse_log_line(line: &str) -> Option<LogEntry> {
    let parts: Vec<&str> = line.splitn(3, ' ').collect();
    if parts.len() < 3 {
        return None;
    }
    Some(LogEntry {
        timestamp: parts[0].parse().ok()?,
        level: parts[1].to_string(),
        message: parts[2].to_string(),
    })
}

fn main() {
    let raw_logs = "\
1000 INFO Server started
1001 DEBUG Connection from 192.168.1.1
1002 ERROR Failed to read database
1003 INFO Request processed in 42ms
1004 WARN Slow query detected
1005 ERROR Connection timeout
1006 INFO Shutting down";

    let entries: Vec<LogEntry> = raw_logs
        .lines()
        .filter_map(parse_log_line)
        .collect();

    let error_count = entries.iter().filter(|e| e.level == "ERROR").count();
    let last_error = entries.iter().rev().find(|e| e.level == "ERROR");

    println!("Total entries: {}", entries.len());
    println!("Errors: {}", error_count);
    if let Some(err) = last_error {
        println!("Last error: {} (t={})", err.message, err.timestamp);
    }

    // Group by level
    let mut by_level: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
    for entry in &entries {
        *by_level.entry(&entry.level).or_insert(0) += 1;
    }
    let mut sorted: Vec<_> = by_level.iter().collect();
    sorted.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
    println!("\nBy level:");
    for (level, count) in sorted {
        println!("  {}: {}", level, count);
    }
}
Total entries: 7
Errors: 2
Last error: Connection timeout (t=1005)

By level:
  INFO: 3
  ERROR: 2
  DEBUG: 1
  WARN: 1

Common Pitfalls

  • Forgetting that iterators are lazy. vec.iter().map(|x| do_something(x)) does nothing without a consumer like collect(), for_each(), count(), or sum().
  • Using collect when you only need one value. If you want the first match, use find, not filter().collect() followed by indexing.
  • Indexing instead of iterating. for i in 0..vec.len() { vec[i] } is less idiomatic and less efficient than for item in &vec. Bounds checks are eliminated when the compiler knows you are iterating the whole collection.
  • Creating intermediate collections unnecessarily. Chain iterator methods instead of collecting into a Vec just to iterate again.
  • Confusing iter() and into_iter(). iter() borrows. into_iter() consumes. If you need the collection after iteration, use iter().
  • Not using filter_map for combined filter + map. When your map function returns Option, filter_map handles both steps cleanly.

Key Takeaways

  • .iter() borrows, .iter_mut() borrows mutably, .into_iter() consumes.
  • Iterator chains compile to the same code as hand-written loops. Zero overhead.
  • Iterators are lazy. Nothing executes until a consumer (.collect(), .sum(), .count(), etc.) drives the chain.
  • collect can produce any collection type that implements FromIterator.
  • Prefer iterator chains over manual loops. They are more readable, more composable, and equally fast.
  • Use filter_map for "try to transform, skip failures" and flat_map for "transform into sequences, then flatten."