Rust Iterators: A Guide

By the time you walk out of here, you should understand what iterators are good for, how they work internally, and how to create your own!

Responses (1)

goodsw4all3 months ago

Thanks for giving me better understanding of iterator in Rust

Clap
1|1|

Iterators are a fairly central concept to Rust. If you're looping over something, you're very likely already using an iterator. If you're transforming collections, you probably should be using them. If your function returns a lazily evaluated sequence of things, you should consider returning an iterator - especially if that sequence could be lazily evaluated.

WYWL: What you will learn#

We'll take a look at how iterators are implemented, how to iterate over a collection, what sorts of iterators exist in the standard library, usage and common patterns for transforming data, and finally a few examples of useful crates that provide their functionality via powerful iterators.

Prerequisites#

Skill-wise, you'll ideally have an understanding of

  • structs and enums,

  • the Option<T> generic type,

  • traits, and

  • just a pinch of closures,

I highly recommend having some sort of environment to run snippets of code in. The simplest thing to use is the Rust Playground. If you'd like a local environment, refer to The Book for guidance on setting that up.

What is an iterator?#

Essentially, an iterator is a thing that allows you to traverse some sort of a sequence. Note that since Rust's iterators are lazy, this sequence could be generated on the fly - you could just as well traverse an existing array of finite length or create an iterator that keeps spewing out random numbers infinitely.

Laziness in programming is this general idea of delaying a computation until it's actually needed. A lazy iterator doesn't need to know all the elements it's going to return when it's first initialized - it can compute every next element when/if it's asked for.

The Iterator trait#

In Rust, iterators are typically implemented using the Iterator trait. All we need to implement that trait for our custom type is provide an associated type Item (this is the type of the elements of the sequence, returned by the iterator) and the next method.

Let's try and implement an iterator over numbers from 1 to 10.

The next method is expected to yield the next element of the sequence. It takes a mutable reference to self in case we need to keep track of some state between next calls, which is normally the case. We'll soon find it useful.

The return type of next is Option<Self::Item>. If an iterator is finite, it needs to return None to indicate it has no more elements to return. Right now, this iterator will immediately finish, not yielding any items. Let's fix this.

This should be pretty self-explanatory. Every call to next increments self.currentand yields it until it grows beyond 10.

Looping#

So we have an iterator. Let's use it. The most basic thing we can do is simply loop over all of its elements:

There's actually an Iterator implementation for the Range type in Rust! A Range is what you get when you type something like 1..5. Instead of writing the above custom iterator, we could have simply done this:

IntoIterator, iter() and iter_mut()#

Sometimes you'll want to provide the user of your code the ability to iterate over your type, but without that type itself being an iterator. This will be the case with collections. If you implement your own vector type, you probably don't want that type to needlessly hold an extra iteration variable just in case someone wants to iterate over it.

What we want is a way to create an iterator out of the collection. There are three mechanisms you'll typically see.

  • The IntoIterator trait is implemented for the type and provides you with the into_iter method. This one consumes the data and wraps it in an owning iterator.

  • The iter method is defined directly on the type. This method will borrow the data immutably and return an iterator that provides immutable references.

  • The iter_mut method is defined directly on the type. This method will borrow the data mutably and return an iterator that provides mutable references.

If we want to iterate over a vector of chars, we could do something like this:

If we leave out the into_iter() call, Rust will call that method implicitly anyway.

This implicit call is important to keep in mind. Since into_iter() consumes the data, we cannot use the original vector later.

One solution would be to explicitly call v.iter() so that the iterator is borrowing instead. Another is to provide a reference to v rather than the owned value.