Web Analytics

Iterators

Advanced ~40 min read

An iterator is a way of processing a series of items. In Rust, iterators are lazy, meaning they don't do any work until you call methods that consume the iterator. Iterators are one of Rust's most powerful features, allowing you to write efficient, functional-style code that's both readable and performant.

What is an Iterator?

An iterator is a value that produces a sequence of values. The Iterator trait is defined in the standard library and has one required method: next():

trait Iterator {
    type Item;
    
    fn next(&mut self) -> Option<Self::Item>;
}
Iterator Trait: The Iterator trait is in the prelude, so you don't need to import it. It provides many useful methods for working with sequences of values.

Creating Iterators

Most collections in Rust can produce iterators. There are three main methods:

1. iter() - Borrows Elements

let v = vec![1, 2, 3];
for item in v.iter() {
    println!("{}", item);
}
// v is still valid after iteration

2. into_iter() - Takes Ownership

let v = vec![1, 2, 3];
for item in v.into_iter() {
    println!("{}", item);
}
// v is no longer valid - it was moved

3. iter_mut() - Mutable References

let mut v = vec![1, 2, 3];
for item in v.iter_mut() {
    *item += 10;
}
// v is modified: [11, 12, 13]
Choosing the Right Method: Use iter() when you only need to read values, into_iter() when you need to consume the collection, and iter_mut() when you need to modify values.
Output
Click Run to execute your code

Using next() Directly

You can call next() directly to get values one at a time:

let v = vec![1, 2, 3];
let mut iter = v.iter();

println!("{:?}", iter.next());  // Some(&1)
println!("{:?}", iter.next());  // Some(&2)
println!("{:?}", iter.next());  // Some(&3)
println!("{:?}", iter.next());  // None
Returns Option: The next() method returns Option<Item>. When there are more items, it returns Some(item). When the iterator is exhausted, it returns None.

Lazy Evaluation

Iterators are lazy - they don't do any work until you consume them:

let v = vec![1, 2, 3, 4, 5];
let iter = v.iter().map(|x| {
    println!("Processing {}", x);
    x * 2
});
// Nothing printed yet - iterator is lazy!

let doubled: Vec<i32> = iter.collect();  // Now it executes
// Processing 1, Processing 2, ...
Lazy Evaluation: Because iterators are lazy, you must consume them (e.g., with collect(), sum(), or a for loop) for them to actually do work.

Iterator Consumers

Methods that consume the iterator and produce a final value:

Method Description Example
collect() Collect into collection iter.collect::<Vec<_>>()
sum() Sum all elements iter.sum()
product() Multiply all elements iter.product()
max() Find maximum iter.max()
min() Find minimum iter.min()
count() Count elements iter.count()
any() Check if any element matches iter.any(|x| x > 5)
all() Check if all elements match iter.all(|x| x > 0)

Iterator Adapters

Iterator adapters transform one iterator into another. They're lazy and return new iterators:

map() - Transform Each Element

let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter()
    .map(|x| x * 2)
    .collect();
// [2, 4, 6]

filter() - Keep Matching Elements

let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = numbers.iter()
    .filter(|x| *x % 2 == 0)
    .collect();
// [2, 4]

take() - Take First N Elements

let numbers = vec![1, 2, 3, 4, 5];
let first_three: Vec<&i32> = numbers.iter()
    .take(3)
    .collect();
// [1, 2, 3]

skip() - Skip First N Elements

let numbers = vec![1, 2, 3, 4, 5];
let skipped: Vec<&i32> = numbers.iter()
    .skip(2)
    .collect();
// [3, 4, 5]

enumerate() - Add Index

let items = vec!["a", "b", "c"];
for (i, item) in items.iter().enumerate() {
    println!("{}: {}", i, item);
}
// 0: a, 1: b, 2: c

zip() - Combine Two Iterators

let names = vec!["Alice", "Bob"];
let ages = vec![30, 25];
let people: Vec<_> = names.iter()
    .zip(ages.iter())
    .collect();
// [("Alice", 30), ("Bob", 25)]

chain() - Concatenate Iterators

let v1 = vec![1, 2, 3];
let v2 = vec![4, 5, 6];
let chained: Vec<&i32> = v1.iter()
    .chain(v2.iter())
    .collect();
// [1, 2, 3, 4, 5, 6]

flat_map() - Map and Flatten

let words = vec!["hello", "world"];
let chars: Vec<char> = words.iter()
    .flat_map(|s| s.chars())
    .collect();
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

rev() - Reverse Iterator

let numbers = vec![1, 2, 3];
let reversed: Vec<&i32> = numbers.iter()
    .rev()
    .collect();
// [3, 2, 1]

Chaining Multiple Adapters

let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers.iter()
    .filter(|x| *x % 2 == 0)  // Keep evens
    .map(|x| x * x)            // Square them
    .take(3)                   // Take first 3
    .copied()                   // Copy values
    .collect();
// [4, 16, 36]
Output
Click Run to execute your code
Efficient Chaining: Iterator adapters can be chained efficiently because they're lazy. The compiler can optimize the entire chain into a single loop.

Creating Custom Iterators

You can create your own iterators by implementing the Iterator trait:

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Counter {
        Counter { count: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

let counter = Counter::new(5);
for num in counter {
    println!("{}", num);  // 1, 2, 3, 4, 5
}
type Item: The associated type Item defines what type of value the iterator yields. This is required when implementing Iterator.

Infinite Iterators

Iterators can be infinite - just never return None:

struct Fibonacci {
    curr: u32,
    next: u32,
}

impl Iterator for Fibonacci {
    type Item = u32;
    
    fn next(&mut self) -> Option<Self::Item> {
        let current = self.curr;
        self.curr = self.next;
        self.next = current + self.next;
        Some(current)
    }
}

let fib: Vec<u32> = Fibonacci::new()
    .take(10)  // Limit infinite iterator
    .collect();
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Infinite Iterators: Be careful with infinite iterators! Always use methods like take() to limit them, or you'll get an infinite loop.
Output
Click Run to execute your code

When to Use Iterators

Use iterators when:

  • Processing collections: Transforming, filtering, or aggregating data
  • Functional style: When you prefer functional programming patterns
  • Performance: Iterators are often optimized by the compiler
  • Readability: Iterator chains can be more readable than loops
  • Lazy evaluation: When you want to defer computation

Consider loops when:

  • Simple iteration: When you just need to iterate without transformation
  • Early exit needed: When you need break/continue
  • Side effects: When the primary purpose is side effects, not transformation
Performance: Iterators are often as fast as (or faster than) hand-written loops because the compiler can optimize them. Don't avoid iterators for performance reasons - benchmark first!

Best Practices

  • Prefer iterators for transformations: map, filter, etc. are more idiomatic
  • Chain adapters efficiently: Multiple adapters are optimized together
  • Use collect() when needed: Remember iterators are lazy
  • Choose the right iterator method: iter(), into_iter(), or iter_mut()
  • Implement Iterator for custom types: When you need custom iteration logic
  • Use take() for infinite iterators: Always limit infinite iterators
  • Consider performance: Iterators are usually fast, but benchmark if needed

Common Mistakes

1. Forgetting that iterators are lazy

// Wrong - Nothing happens!
let doubled = vec![1, 2, 3].iter().map(|x| x * 2);
// doubled is an iterator, not a Vec

// Correct - Consume the iterator
let doubled: Vec<i32> = vec![1, 2, 3].iter()
    .map(|x| x * 2)
    .collect();

2. Using into_iter() when you need the collection later

// Wrong - v is moved
let v = vec![1, 2, 3];
for item in v.into_iter() { }
println!("{:?}", v);  // Error: v was moved

// Correct - Use iter() to borrow
let v = vec![1, 2, 3];
for item in v.iter() { }
println!("{:?}", v);  // OK

3. Creating infinite iterators without limits

// Wrong - Infinite loop!
let infinite: Vec<i32> = (1..).collect();  // Never finishes

// Correct - Use take() to limit
let limited: Vec<i32> = (1..).take(10).collect();

4. Not specifying type for collect()

// Wrong - Compiler doesn't know what to collect into
let result = vec![1, 2, 3].iter().map(|x| x * 2).collect();  // Error!

// Correct - Specify type
let result: Vec<i32> = vec![1, 2, 3].iter()
    .map(|x| x * 2)
    .collect();

Exercise: Iterators Practice

Task: Practice using iterators for common operations.

Requirements:

  • Use iterators to find sum of squares of even numbers
  • Create a custom iterator for powers of 2
  • Use zip to combine names and scores
  • Flatten nested vectors using flat_map
  • Create a prime number iterator
Output
Click Run to execute your code
Show Solution
// Sum of squares of evens
let sum: i32 = numbers.iter()
    .filter(|x| *x % 2 == 0)
    .map(|x| x * x)
    .sum();

// Powers of 2 iterator
struct PowersOfTwo {
    current: u32,
    max_power: u32,
}

impl Iterator for PowersOfTwo {
    type Item = u32;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.current <= self.max_power {
            let result = 2_u32.pow(self.current);
            self.current += 1;
            Some(result)
        } else {
            None
        }
    }
}

// Zip and filter
let high_scorers: Vec<_> = names.iter()
    .zip(scores.iter())
    .filter(|(_, &score)| score >= 90)
    .collect();

// Flatten nested vector
let flattened: Vec<i32> = nested.iter()
    .flat_map(|v| v.iter())
    .copied()
    .collect();

Summary

  • Iterators process sequences of values
  • Three main methods: iter(), into_iter(), iter_mut()
  • Iterators are lazy - they don't work until consumed
  • Iterator adapters transform iterators (map, filter, take, etc.)
  • Iterator consumers produce final values (collect, sum, etc.)
  • Implement Iterator trait to create custom iterators
  • Use take() to limit infinite iterators
  • Iterators are often as fast as loops (compiler optimizes them)
  • Chain adapters for complex transformations

What's Next?

Now that you understand iterators, you'll learn about closures - anonymous functions that can capture their environment. Closures are often used with iterators (like in map() and filter()) and are a powerful feature for writing concise, functional-style code.