Responses (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.
/// An iterator that returns numbers from 1 to 10.
struct OneToTen {
current: u32,
}
impl OneToTen {
/// Constructor for OneToTen.
fn new() -> Self {
Self {
current: 0,
}
}
}
impl Iterator for OneToTen {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
None // we'll change this in a second...
}
}
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.
impl Iterator for OneToTen {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < 10 {
self.current += 1;
return Some(self.current);
}
None
}
}
This should be pretty self-explanatory. Every call to next
increments self.current
and 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:
fn main() {
for i in OneToTen::new() {
println!("{}", i);
}
}
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:
fn main() {
for i in 1..11 {
println!("{}", i);
}
}
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 theinto_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:
fn main() {
let v = vec!('a', 'b', 'c');
for c in v.into_iter() {
println!("{}", c);
}
}
If we leave out the into_iter()
call, Rust will call that method implicitly anyway.
fn main() {
let v = vec!('a', 'b', 'c');
for c in v {
println!("{}", c);
}
}
This implicit call is important to keep in mind. Since into_iter()
consumes the data, we cannot use the original vector later.
fn main() {
let v = vec!('a', 'b', 'c');
for c in v {
println!("{}", c);
}
println!("{:?}", v);
// error: borrow of moved value: `v`
}
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.
fn main() {
let v = vec!('a', 'b', 'c');
for c in &v {
println!("{}", c);
}
Thanks for giving me better understanding of iterator in Rust