Processando uma Série de Itens com Iteradores
O padrão de iterador permite executar alguma tarefa sobre uma sequência de itens, um de cada vez. Um iterador é responsável pela lógica de percorrer cada item e de determinar quando a sequência terminou. Quando você usa iteradores, não precisa reimplementar essa lógica por conta própria.
Em Rust, iteradores são preguiçosos, o que significa que eles não fazem nada
até que você chame métodos que consumam o iterador para efetivamente usá-lo.
Por exemplo, o código da Listagem 13-10 cria um iterador sobre os itens do
vetor v1 chamando o método iter definido em Vec<T>. Sozinho, esse código
não faz nada de útil.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
O iterador é armazenado na variável v1_iter. Depois de criar um iterador,
podemos usá-lo de várias maneiras. Na Listagem 3-5, iteramos sobre um array
usando um laço for para executar algum código sobre cada um de seus itens.
Nos bastidores, isso criou e depois consumiu um iterador de forma implícita,
mas deixamos de lado como exatamente isso funcionava até agora.
No exemplo da Listagem 13-11, separamos a criação do iterador do uso do
iterador no laço for. Quando o laço for é executado usando o iterador em
v1_iter, cada elemento do iterador é usado em uma iteração do laço, o que
imprime cada valor.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
forEm linguagens que não têm iteradores fornecidos por suas bibliotecas padrão, você provavelmente escreveria essa mesma funcionalidade criando uma variável inicializada com índice 0, usando essa variável para indexar o vetor e obter um valor, e incrementando a variável em um laço até ela atingir o número total de itens no vetor.
Os iteradores cuidam de toda essa lógica para você, reduzindo a quantidade de código repetitivo que você poderia facilmente escrever errado. Eles também dão mais flexibilidade para reutilizar a mesma lógica com muitos tipos diferentes de sequências, não apenas estruturas de dados que podem ser indexadas, como vetores. Vamos ver como os iteradores fazem isso.
A Trait Iterator e o Método next
Todos os iteradores implementam uma trait chamada Iterator, definida na
biblioteca padrão. A definição dessa trait se parece com isto:
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
}
Observe que essa definição usa uma sintaxe nova: type Item e Self::Item,
que definem um tipo associado a essa trait. Falaremos de tipos associados em
detalhes no Capítulo 20. Por enquanto, tudo o que você precisa saber é que esse
código diz que implementar a trait Iterator exige que você também defina um
tipo Item, e esse tipo é usado no valor de retorno do método next. Em
outras palavras, Item será o tipo retornado pelo iterador.
A trait Iterator exige apenas que implementadores definam um método: next,
que retorna um item do iterador por vez, envolto em Some, e, quando a
iteração termina, retorna None.
Podemos chamar o método next diretamente em iteradores; a Listagem 13-12
demonstra quais valores são retornados por chamadas repetidas a next no
iterador criado a partir do vetor.
#[cfg(test)]
mod tests {
#[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);
}
}
next em um iteradorObserve que tivemos de tornar v1_iter mutável: chamar next em um iterador
altera o estado interno que ele usa para controlar onde está na sequência. Em
outras palavras, esse código consome, ou esgota, o iterador. Cada chamada a
next consome um item do iterador. Não precisamos tornar v1_iter mutável
quando usamos um laço for, porque o laço tomou ownership de v1_iter e o
tornou mutável nos bastidores.
Observe também que os valores obtidos das chamadas a next são referências
imutáveis aos valores do vetor. O método iter produz um iterador sobre
referências imutáveis. Se quisermos criar um iterador que tome ownership de
v1 e retorne valores possuídos, podemos chamar into_iter em vez de iter.
Da mesma forma, se quisermos iterar sobre referências mutáveis, podemos chamar
iter_mut em vez de iter.
Métodos que Consomem o Iterador
A trait Iterator possui vários métodos com implementações padrão fornecidas
pela biblioteca padrão; você pode conhecê-los consultando a documentação da API
de Iterator. Alguns desses métodos chamam next em sua definição, e é por
isso que você precisa implementar next ao implementar a trait Iterator.
Métodos que chamam next são chamados de adaptadores consumidores, porque
chamá-los esgota o iterador. Um exemplo é o método sum, que toma ownership
do iterador e percorre seus itens chamando next repetidamente, consumindo-o.
Durante a iteração, ele soma cada item a um total acumulado e retorna esse
total quando a iteração termina. A Listagem 13-13 tem um teste que ilustra o
uso de sum.
#[cfg(test)]
mod tests {
#[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);
}
}
sum para obter o total de todos os itens do iteradorNão temos permissão para usar v1_iter depois da chamada a sum, porque
sum toma ownership do iterador sobre o qual é chamado.
Métodos que Produzem Outros Iteradores
Adaptadores de iteradores são métodos definidos na trait Iterator que não
consomem o iterador. Em vez disso, produzem iteradores diferentes, alterando
algum aspecto do iterador original.
A Listagem 13-14 mostra um exemplo de chamada ao método adaptador map, que
recebe uma closure a ser aplicada a cada item à medida que eles são
percorridos. O método map retorna um novo iterador que produz os itens
modificados. A closure aqui cria um novo iterador em que cada item do vetor é
incrementado em 1.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
map para criar um novo iteradorEntretanto, esse código gera um aviso:
$ 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`
O código da Listagem 13-14 não faz nada; a closure que especificamos nunca é chamada. O aviso nos lembra o motivo: adaptadores de iteradores são preguiçosos, e aqui precisamos consumir o iterador.
Para corrigir esse aviso e consumir o iterador, usaremos o método collect,
que já usamos com env::args na Listagem 12-1. Esse método consome o iterador
e reúne os valores resultantes em um tipo de coleção.
Na Listagem 13-15, coletamos os resultados da iteração sobre o iterador
retornado pela chamada a map em um vetor. Esse vetor acabará contendo cada
item do vetor original, incrementado em 1.
fn main() {
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]);
}
map para criar um novo iterador e depois collect para consumi-lo e criar um vetorComo map recebe uma closure, podemos especificar qualquer operação que
quisermos realizar sobre cada item. Esse é um ótimo exemplo de como closures
permitem personalizar um comportamento ao mesmo tempo que reutilizam o
comportamento de iteração fornecido pela trait Iterator.
Você pode encadear várias chamadas a adaptadores de iteradores para executar ações complexas de maneira legível. Mas, como todos os iteradores são preguiçosos, é necessário chamar um dos métodos consumidores para obter resultados das chamadas a esses adaptadores.
Closures que Capturam o Ambiente
Muitos adaptadores de iteradores recebem closures como argumentos, e frequentemente as closures que especificamos nesses casos são closures que capturam o ambiente onde foram definidas.
Neste exemplo, usaremos o método filter, que recebe uma closure. A closure
recebe um item do iterador e retorna um bool. Se a closure retornar true,
o valor será incluído na iteração produzida por filter. Se retornar false,
o valor não será incluído.
Na Listagem 13-16, usamos filter com uma closure que captura a variável
shoe_size do ambiente para iterar sobre uma coleção de instâncias da struct
Shoe. Ela retornará apenas os sapatos que tiverem o tamanho especificado.
#[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")
},
]
);
}
}
filter com uma closure que captura shoe_sizeA função shoes_in_size toma ownership de um vetor de sapatos e de um tamanho
de sapato como parâmetros. Ela retorna um vetor contendo apenas os sapatos do
tamanho especificado.
No corpo de shoes_in_size, chamamos into_iter para criar um iterador que
toma ownership do vetor. Em seguida, chamamos filter para adaptar esse
iterador em um novo iterador que contém apenas os elementos para os quais a
closure retorna true.
A closure captura o parâmetro shoe_size do ambiente e compara esse valor com
o tamanho de cada sapato, mantendo apenas os sapatos do tamanho especificado.
Por fim, chamar collect reúne os valores retornados pelo iterador adaptado em
um vetor, que é retornado pela função.
O teste mostra que, quando chamamos shoes_in_size, recebemos de volta apenas
os sapatos que têm o mesmo tamanho do valor especificado.