Skip to main content

Iterators in Rust

Iterators in Rust allow you to perform tasks sequentially on a series of items while knowing the end of the sequence. Iterator is a trait that, once implemented, handles many of the nuances of safely looping through collections. Iterators are lazy, meaning they are only used when called upon.

Creating an Iterator

Most collections we discussed earlier have the Iterator trait already implemented. The most common usage of iterators regards arrays or vectors, and they are often used as an alternative for loops:

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

// Call the `.iter()` method on `Vec`:
let iter = my_vec.iter();
for val in iter {
println!("Value: {}", val);
}
// Optionally, call it in-line:
let my_vec = vec![1, 2, 3, 4, 5];
for val in my_vec.iter() {
println!("Value: {}", val);
}

You may notice that this looks familiar to a regular for loop. This will be discussed in more depth in a later section, but know they are different!

A closer look at the Iterator trait

Let's examine the Iterator trait itself:

pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}

The next() method is the heart of an Iterator's functionality, as it's the only method needed to implement the Iterator trait. next() returns the next item as Some(value) from the iterator, and returns None once it's complete.

Notice that although Iterator can iterate over many types, it uses an associated type, Item. Using an associated type allows the type implementing the trait to specify the desired outcome, versus a generic would have the developer impose some type that may be incorrect.

"Consuming" An Iterator & Iterator Methods

As a result of implementing Iterator, one can use a few key methods to modify a sequence of items. For more information on available methods, it is highly encouraged to read through the Rust documentation for the Iterator trait.

Some methods consume an iterator, meaning they call the next( ) method within their implementation. An example is .sum(), which collectively adds all items by calling next() until the iterator has reached its end:

let my_vec = vec![1, 2, 3, 4, 5];
let sum = my_vec.iter().sum();

The .map() method is very commonly used. It takes a closure, performs an operation over each item in the iterator, and returns an iterator. This does not modify the previous iterator, rather it returns a new one with modified values:

let my_vec = vec![1, 2, 3, 4, 5];
// Returns a new iterator that adds `1` to each item
my_vec.iter().map(|x| x + 1);

Another method that operates similarly is filter() , which returns an iterator of filtered values based on a conditional statement within the closure:

let my_vec = vec![1, 2, 3, 4, 5];
// Returns a new iterator that adds `1` to each item
my_vec.iter().filter(|x| *x % 2 == 0);

Using .collect()

Using collect(), you can gather the values of an iterator and back into a vector that represents the operation you performed on it:

let my_vec = vec![1, 2, 3, 4, 5];
// Returns a new iterator containing only even numbers
let filter_iter = my_vec.iter().filter(|x| *x % 2 == 0);
// Returns a new iterator that adds `1` to each item
let map_iter = my_vec.iter().map(|x| x + 1);

// Collect into a filtered Vec
let even: Vec<_> = filter_iter.collect(); // [2, 4]
// Collect into a mapped Vec
let plus_one: Vec<_> = map_iter.collect(); // [2, 3, 4, 5, 6]

As some of these methods return an iterator, it's possible to chain them:

// Both maps then filter on the returned iterator from `map`
let chained_iter = my_vec.iter().map(|x| x + 1).filter(|x| *x % 2 == 0);

Loops vs Iterators - when to use which?

In the previous section, a piece of code looked just like a for loop that was covered in the Loops module:

    let my_vec = vec![1, 2, 3, 4, 5];
for val in my_vec.iter() {
println!("Value: {}", val);
}

At first glance, this appears to be the same syntax and functionality as a normal for loop:

    let my_vec = vec![1, 2, 3, 4, 5];
for val in my_vec {
println!("Value: {}", val);
}

Both serve the same purpose but behave differently. Iterators operate on references, whereas a for loop directly takes possession of the value for the loop's scope. Calling:

for val in my_vec.iter() {}

It is effectively the same as calling:

for val in &my_vec {}

It is generally a better design decision to utilize a reference-based approach, as my_vec would be unusable after a traditional for loop that takes ownership.

Try it yourself!

What's going on here?

The code features a collection of various iterators stemming from the same Vec, my_vec. Iterators do not directly modify the Vec. Rather each operation returns a new iterator with values that may be modified, which is the case with map().