Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Listing 13-10: Criando um iterador

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.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 13-11: Usando um iterador em um laço for

Em 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.

Filename: src/lib.rs
#[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);
    }
}
Listing 13-12: Chamando o método next em um iterador

Observe 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.

Filename: src/lib.rs
#[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);
    }
}
Listing 13-13: Chamando o método sum para obter o total de todos os itens do iterador

Nã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.

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
Listing 13-14: Chamando o adaptador de iterador map para criar um novo iterador

Entretanto, 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.

Filename: src/main.rs
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]);
}
Listing 13-15: Chamando map para criar um novo iterador e depois collect para consumi-lo e criar um vetor

Como 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.

Filename: src/lib.rs
#[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")
                },
            ]
        );
    }
}
Listing 13-16: Usando o método filter com uma closure que captura shoe_size

A 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.