mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 04:54:17 -06:00
275 lines
10 KiB
Markdown
275 lines
10 KiB
Markdown
# Processing a Series of Items with Iterators
|
|
Iterator patterns allow you to perform some task on a sequence of items in turn.
|
|
|
|
An iterator is responsibl for the logic of iterating over each item and determining when the sequence has finished.
|
|
|
|
When you use iterators, you don't have to reimplement that logic yourself again.
|
|
|
|
Rust's iterators are *lasy*, meaning they have no effect until yo call methods that consume the iterator to use it up
|
|
|
|
In this example the code creates an iterator over the items in the vecotr `v1` by calling the `iter` method defined on `Vec<T>`
|
|
|
|
This code does nothing useful.
|
|
```rust
|
|
let v1 = vec![1, 2, 3];
|
|
|
|
let v1_iter = v1.iter();
|
|
```
|
|
The iterator is stored in the `v1_iter` variable.
|
|
|
|
Once an iterator is created, we can use it in a variety of ways.
|
|
|
|
Before we iterator over an array using a `for` loop to execute some code on each of its items.
|
|
|
|
Under the hood this implicitly created and then consumed an iterator, but we glossed over how exactly that works until now.
|
|
|
|
In this example, we separate the creation of the iterator from the use of the iterator in the `for` loop.
|
|
|
|
When the `for` loop is called using th iterator in `v1_iter`, each element in the iterator is used in one iteration of the loop which prints out each value.
|
|
```rust
|
|
let v1 = vec![1, 2, 3];
|
|
|
|
let v1_iter = v1.iter();
|
|
|
|
for val in v1_iter {
|
|
println!("Got: {val}");
|
|
}
|
|
```
|
|
In languages that don't have iterators provided by their std libraries, you would likely wirte this same functionality by starting at index 0.
|
|
|
|
Using that variable to index into th vector to get a value and incrememting the variable in a loop until it reached the total number of items in the vector.
|
|
|
|
Iterators handle all that logic for you, removing repetitive code that you could mess up.
|
|
|
|
Iterators gives mroe flexibility to use the same logic with many different kinds of sequences, not just data structs you can index into (vectors for example).
|
|
|
|
## The `Iterator` Trait and the `next` Method
|
|
All iterators implement a trait named `Iterator` that is defined in the std library
|
|
|
|
The defintion of the `Iterator` trait looks like this:
|
|
```rust
|
|
pub trait Iterator {
|
|
type Item;
|
|
|
|
fn next(&mut self) -> Option<Self::Item>;
|
|
|
|
// methods with default implementations elided
|
|
}
|
|
```
|
|
|
|
Note this definition uses some new syntax, `type Item` and `Self::Item`, which are defining an *associated type* with this trait
|
|
|
|
Associated types will be discussed in ch20 (Advanced Features)
|
|
|
|
For now know that this code says implementing the `Iterator` trait requires you also defined an `Item` type.
|
|
|
|
This `Item` type is usd in the return in the return type of the `next` method.
|
|
|
|
The `Item` tpye ill be the type returned form the iterator.
|
|
|
|
The `Iterator` trait only requires implmentors to define one method; `next` method which returns one item of the iterator at a time wrapped in `Some` and when the iteration is over, returns `None`.
|
|
|
|
We can call the `next` method on iterators directly
|
|
|
|
This example demonstrates what values are returned from repeated calls to `next` on the iterator created from the vector.
|
|
```rust
|
|
#[test]
|
|
fn iterator_demonstration() {
|
|
let v1 = vec![1, 2, 3];
|
|
|
|
let mut v1_iter = v1.iter();
|
|
|
|
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);
|
|
}
|
|
```
|
|
|
|
Notice that we need to make `v1_iter` mutable.
|
|
|
|
Calling the `next` method on an iterator changes internal state that the iterator uses to keep track of where it is in the sequence.
|
|
|
|
It could also be said that the code *consumes*, or uses up, the iterator.
|
|
|
|
Each call to `next` comsumes an item form the iterator.
|
|
|
|
We didn't need to make `v1_iter` mutable when we used a `for` loop because the loop took ownership of `v1_iter` and made it mutable under the hood.
|
|
|
|
Note as well the values that we get from the `next` are immutable reference to the values in the vector.
|
|
|
|
The `iter` mthod produces an iterator over immutable references.
|
|
|
|
If we want to create an iterator that takes ownership of `v1` and returns owned values, we can call `into_iter`.
|
|
|
|
If you want to iterate over mutalbe references, you can call `iter_mut`
|
|
|
|
## Methods that Consume the Iterator
|
|
The `Iterator` trait has a number of different methods with default implementations provided by the std library.
|
|
|
|
You can find out about these by looking the std library API documentation for the `Iterator` trait.
|
|
|
|
Some these methods call the `next` method in their definition, which is why you are required to implement the `Iterator` trait.
|
|
|
|
Methods that call `next` are called *consuming adapters*, becaise calling them uses up the iterator.
|
|
|
|
One example is the `sum` metod which takes ownership of the iterator and iterates through the items by repeatedly calling `next` thus consuming the iterator.
|
|
|
|
As it iterates through it adds each item to a running total, this comsumes the iterator.
|
|
|
|
When it is complete it return the total
|
|
|
|
Here is a test illustrating a use of the `sum` method
|
|
```rust
|
|
#[test]
|
|
fn iterator_sum() {
|
|
let v1 = vec![1, 2, 3];
|
|
|
|
let v1_iter = v1.iter();
|
|
|
|
let total: i32 = v1_iter.sum();
|
|
|
|
assert_eq!(total, 6);
|
|
}
|
|
```
|
|
We aren't allowed to use `v1_iter` after the call to `sum because it takes ownership of the iterator we call it on.
|
|
|
|
## Methods that Produce Other Iterators
|
|
*Iterator adapters* are methods defined on the `Iterator` trait that don't consume the iterator.
|
|
|
|
Instead, they produce different iterators by changing some aspect of the original iterator.
|
|
|
|
This example shows calling an iterator adapter method `map`, which takes a closure to call on each item as the items are iterated through.
|
|
|
|
The `map` method returns a new iterator that produces the modified items.
|
|
|
|
The closure here creates a new iterator in which each item from the vector will be incremeted by 1
|
|
```rust
|
|
let v1: Vec<i32> = vec![1, 2, 3];
|
|
|
|
v1.iter().map(|x| x + 1);
|
|
```
|
|
But this code produces a warning
|
|
```
|
|
$ cargo run
|
|
Compiling iterators v0.1.0 (file:///projects/iterators)
|
|
warning: unused `Map` that must be used
|
|
--> src/main.rs:4:5
|
|
|
|
|
4 | v1.iter().map(|x| x + 1);
|
|
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
|
= note: iterators are lazy and do nothing unless consumed
|
|
= note: `#[warn(unused_must_use)]` on by default
|
|
help: use `let _ = ...` to ignore the resulting value
|
|
|
|
|
4 | let _ = v1.iter().map(|x| x + 1);
|
|
| +++++++
|
|
|
|
warning: `iterators` (bin "iterators") generated 1 warning
|
|
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
|
|
Running `target/debug/iterators`
|
|
```
|
|
|
|
This doesnt do anything. The closure we sepcified never gets called. The warning reminds us why: iterator adapters are lazy and we need to consume the iterator here.
|
|
|
|
To fix this warning and consume the iterator, we will use the `collect` method, which we used in Ch 12 with `env::args`.
|
|
|
|
This method consumes the iterator and collects the resulting values into a collection data type.
|
|
|
|
In this next example we collect the results of the iterating over the iterator that is returned form the call to `map` into a vector.
|
|
|
|
This vector will end up containing each item form the original vector incremeted by 1.
|
|
```rust
|
|
let v1: Vec<i32> = vec![1, 2, 3];
|
|
|
|
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
|
|
|
|
assert_eq!(v2, vec![2, 3, 4]);
|
|
```
|
|
|
|
Because `map` takes a closure, we can specify any operation we want to perform on each item.
|
|
|
|
This is a good eample of how closures let you do custome behavior while reusing the iteration behavior that the `Iterator` trait provides.
|
|
|
|
You can chain mutliple calls to iterator adapters to perform compex action is a readable way.
|
|
|
|
Due to all iterators being lasy, you have to call one of the consuming adapter methods to get results ffrom calls to iterator adapters.
|
|
|
|
## Using Closures that Capture Their Environment
|
|
Many iterator adapters take closures as args and commonly the closures we will specify as ags to iterator adapters will be closures that capture their environment.
|
|
|
|
In this example we use the `filter` method that takes a closure.
|
|
|
|
The closure get an item form the iterator and returns a `bool`
|
|
|
|
If the closure returns `true`, the value will be included in the iteration produced by `filter`.
|
|
|
|
If the closure returns `false`, the value won't be included.
|
|
|
|
Here we use `filer` with a closure that captures the `shoe_size` variable from its environment to iterate over a collection of `Shoe` struct instances.
|
|
|
|
It will return only shoes that are the specified size.
|
|
```rust
|
|
#[derive(PartialEq, Debug)]
|
|
struct Shoe {
|
|
size: u32,
|
|
style: String,
|
|
}
|
|
|
|
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
|
|
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn filters_by_size() {
|
|
let shoes = vec![
|
|
Shoe {
|
|
size: 10,
|
|
style: String::from("sneaker"),
|
|
},
|
|
Shoe {
|
|
size: 13,
|
|
style: String::from("sandal"),
|
|
},
|
|
Shoe {
|
|
size: 10,
|
|
style: String::from("boot"),
|
|
},
|
|
];
|
|
|
|
let in_my_size = shoes_in_size(shoes, 10);
|
|
|
|
assert_eq!(
|
|
in_my_size,
|
|
vec![
|
|
Shoe {
|
|
size: 10,
|
|
style: String::from("sneaker")
|
|
},
|
|
Shoe {
|
|
size: 10,
|
|
style: String::from("boot")
|
|
},
|
|
]
|
|
);
|
|
}
|
|
}
|
|
```
|
|
The `shoes_in_size` function takes ownership of a vector of shoes and a shoe size parameters.
|
|
|
|
It returns a vector containing only shoes of the specified size.
|
|
|
|
In the body of `shoes_in_size` we call `into_iter` to create an iterator that takes ownership of the vector.
|
|
|
|
We then call `filter` to adapt that iterator into a new iterator that only contains elements for which the closure returns `true`.
|
|
|
|
The closure captures the `shoe_size` parameter from the environment and compares the value with each shoe's size, keeping only shoes of the size specified.
|
|
|
|
Calling `collect` finally gathers the values returned by the adapted iterator into a vector that is returned by the function.
|
|
|
|
This test shows that when we call `shoes_in_size`, we get back only shoes that have the same size as te value specified. |