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.
Skill-wise, you'll ideally have an understanding of
structs and enums,
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
Let's try and implement an iterator over numbers from 1 to 10.
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
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
self.currentand yields it until it grows beyond 10.
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.
IntoIteratortrait is implemented for the type and provides you with the
into_itermethod. This one consumes the data and wraps it in an owning iterator.
itermethod is defined directly on the type. This method will borrow the data immutably and return an iterator that provides immutable references.
iter_mutmethod 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.