rust programming

Iterators in Rust

Introduction

Iterators are a powerful and flexible tool in Rust for processing sequences of elements. They provide a way to traverse collections (such as arrays, vectors, and linked lists) while maintaining memory safety and performance.

Core Concepts

What is an Iterator?

An iterator in Rust is defined by the Iterator trait:

pub trait Iterator {
    type Item;
    
    fn next(&mut self) -> Option<Self::Item>;
    
    // Many other methods with default implementations
}

Key Principles

  1. Lazy Evaluation: Iterators don't compute values until requested
  2. Zero-Cost Abstraction: Iterator operations compile to efficient machine code
  3. Memory Safety: Iterators respect Rust's ownership and borrowing rules
  4. Chainable Operations: Multiple iterator adaptors can be combined
  5. Type Safety: Iterator operations are checked at compile time

Creating Iterators

There are three main ways to create an iterator:

let collection = vec![1, 2, 3, 4, 5];

// 1. Immutable references
let iter = collection.iter();        // &T

// 2. Mutable references
let iter_mut = collection.iter_mut(); // &mut T

// 3. Taking ownership
let into_iter = collection.into_iter(); // T

Basic Usage Example

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Using next() manually
    let mut iter = numbers.iter();
    assert_eq!(iter.next(), Some(&1));
    assert_eq!(iter.next(), Some(&2));
    assert_eq!(iter.next(), Some(&3));
    
    // Using for loop (more common)
    for number in numbers.iter() {
        println!("Got: {}", number);
    }
}

Iterator Adaptors

Iterator adaptors transform an iterator into another kind of iterator. They are lazy and must be consumed to produce a result.

Common Adaptors

  1. map: Transform each element
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter()
    .map(|x| x * 2)
    .collect();
// doubled = [2, 4, 6]
  1. filter: Keep elements that match a predicate
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter()
    .filter(|x| *x % 2 == 0)
    .collect();
// evens = [2, 4, 6]
  1. enumerate: Add indices to elements
let chars = vec!['a', 'b', 'c'];
for (index, &c) in chars.iter().enumerate() {
    println!("{}. {}", index, c);
}
  1. zip: Combine two iterators
let numbers = vec![1, 2, 3];
let letters = vec!['a', 'b', 'c'];
let pairs: Vec<_> = numbers.iter()
    .zip(letters.iter())
    .collect();
// pairs = [(1, 'a'), (2, 'b'), (3, 'c')]

Consuming Adaptors

These methods consume the iterator and produce a final value.

Common Consumers

  1. collect: Gather elements into a collection
let numbers = vec![1, 2, 3, 4, 5];
let squares: Vec<i32> = numbers.iter()
    .map(|x| x * x)
    .collect();
  1. sum: Add up all elements
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
assert_eq!(sum, 15);
  1. fold: Accumulate values with custom logic
let numbers = vec![1, 2, 3, 4, 5];
let product = numbers.iter()
    .fold(1, |acc, &x| acc * x);
assert_eq!(product, 120); // 5!

Advanced Iterator Patterns

1. Chaining Multiple Operations

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
    .filter(|&x| x <= 50)        // Keep <= 50
    .collect();
// result = [4, 16, 36]

2. Creating Custom Iterators

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

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

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

// Usage
let counter = Counter::new(3);
for num in counter {
    println!("{}", num);  // Prints: 1, 2, 3
}

3. Peekable Iterators

let numbers = vec![1, 2, 3, 4, 5];
let mut iter = numbers.iter().peekable();

while let Some(&num) = iter.peek() {
    if num % 2 == 0 {
        iter.next();  // Skip even numbers
        continue;
    }
    println!("Got odd number: {}", iter.next().unwrap());
}

Best Practices

  1. Choose the Right Iterator Method

    • Use iter() for reading
    • Use iter_mut() for modifying in place
    • Use into_iter() when you need ownership
  2. Leverage Iterator Chains

    • Combine operations for cleaner code
    • Let the compiler optimize the chains
  3. Use Type Inference

    // Instead of
    let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();
    
    // You can often use
    let squares = numbers.iter()
        .map(|x| x * x)
        .collect::<Vec<_>>();
    
  4. Consider Performance

    • Iterators are zero-cost abstractions
    • Chain operations before collecting
    • Use filter_map instead of filter().map()

Common Pitfalls

  1. Collecting Too Early
// Inefficient
let even: Vec<i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
let doubled: Vec<i32> = even.iter().map(|x| x * 2).collect();

// Better
let result: Vec<i32> = numbers.iter()
    .filter(|x| *x % 2 == 0)
    .map(|x| x * 2)
    .collect();
  1. Forgetting to Collect
// This doesn't do anything!
numbers.iter().map(|x| x * 2);

// Need to collect or consume
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
  1. Iterator Invalidation
let mut numbers = vec![1, 2, 3];
for i in 0..numbers.len() {
    numbers.push(i);  // Don't modify while iterating!
}

Next Steps

After mastering iterators:

  • Explore the itertools crate for additional iterator combinators
  • Learn about parallel iterators with rayon
  • Study iterator performance optimization
  • Understand how iterators work with async code

Remember: Iterators are one of Rust's most powerful features. They combine safety, efficiency, and expressiveness, making them an essential tool for writing idiomatic Rust code.