Iterators in Rust

In Rust, iterators are a powerful and efficient feature for processing a sequence of items. An iterator is responsible for the logic of iterating over each item in a collection. A key feature of Rust’s iterators is that they are lazy, meaning they have no effect until you call methods that consume them.

The Iterator Trait

The core of Rust’s iteration is the Iterator trait, which is defined in the standard library. It requires only one method to be implemented: next().

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

The next() method returns the next item from the sequence wrapped in Some, or None when the sequence is finished.

Example: Manually Using an Iterator

fn main() {
    let v1 = vec![1, 2, 3];
    let mut v1_iter = v1.iter(); // Create an iterator

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

Creating Iterators from Collections

There are three main ways to create an iterator from a collection like a Vec<T>:

  1. iter(): Returns an iterator over immutable references (&T). The original collection is not changed.
  2. iter_mut(): Returns an iterator over mutable references (&mut T), allowing you to modify the elements.
  3. into_iter(): Returns an iterator that takes ownership of the collection and returns owned values (T). The collection is consumed.
// iter() - immutable references
let v = vec!["a", "b", "c"];
for s in v.iter() {
    println!("{}", s);
}

// iter_mut() - mutable references
let mut v_mut = vec![1, 2, 3];
for n in v_mut.iter_mut() {
    *n *= 2; // Modify the element
}
println!("{:?}", v_mut); // Output: [2, 4, 6]

// into_iter() - owned values
let v_owned = vec![String::from("one"), String::from("two")];
for s in v_owned.into_iter() {
    // s is now an owned String, not a reference
    println!("Owned: {}", s);
}
// v_owned is no longer accessible here

Consuming Adaptors

These are methods that call next() on an iterator until it returns None, thus “consuming” it.

sum()

The sum() method takes ownership of the iterator and sums up all its items.

let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum();
assert_eq!(total, 6);

collect()

The collect() method is a powerful consuming adaptor that gathers the items from an iterator into a new collection.

let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);

Iterator Adaptors

These methods transform an iterator into a new iterator with different behavior. They are lazy and don’t do anything until a consuming adaptor is called.

map()

The map() method takes a closure and creates a new iterator that calls that closure on each element.

let numbers = vec![1, 2, 3];
let plus_one: Vec<_> = numbers.iter().map(|x| x + 1).collect();
println!("{:?}", plus_one); // Output: [2, 3, 4]

filter()

The filter() method takes a closure that returns a boolean. It creates a new iterator that will only yield elements for which the closure returns true.

let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<_> = numbers.into_iter().filter(|n| n % 2 == 0).collect();
println!("{:?}", evens); // Output: [2, 4, 6]

Implementing a Custom Iterator

You can implement the Iterator trait for your own types. Here’s an example of a simple Counter struct that counts from 1 to 5.

struct Counter {
    count: u32,
}

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

impl Iterator for Counter {
    type Item = u32;

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

fn main() {
    let counter = Counter::new();
    for i in counter {
        println!("{}", i); // Prints 1, 2, 3, 4, 5
    }
}

Summary

  • Lazy Evaluation: Iterators do nothing until consumed, leading to efficient code.
  • Zero-Cost Abstraction: Using iterators is just as performant as writing manual loops.
  • Expressive and Composable: Chaining methods like map, filter, and collect leads to concise and readable code.
  • Core to Idiomatic Rust: Iterators are used extensively throughout the standard library and are a fundamental concept for writing effective Rust.