mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-07-07 03:27:13 -06:00
finished ch13.2
This commit is contained in:
275
Iterators.md
Normal file
275
Iterators.md
Normal file
@ -0,0 +1,275 @@
|
||||
# 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.
|
Reference in New Issue
Block a user