Nesta aula, exploraremos técnicas avançadas de iteradores em Rust, essenciais para escrever código idiomático e eficiente. Os iteradores são uma abstração poderosa que permite processar sequências de dados de forma funcional e lazy. Dominar métodos como collect, fold, zip e enumerate ampliará sua capacidade de manipular coleções com concisão e clareza.

Além disso, veremos como criar seus próprios iteradores implementando a trait Iterator, o que é fundamental para integrar tipos personalizados ao ecossistema de iteração do Rust. Por fim, discutiremos aspectos de performance, desmistificando mitos e mostrando como os iteradores podem ser tão rápidos quanto loops manuais.

collect e turbofish

O método collect transforma um iterador em uma coleção concreta, como Vec, HashMap ou String. Ele é um dos métodos mais versáteis e frequentemente usados. No entanto, como collect pode produzir vários tipos de coleção, o compilador precisa saber qual tipo específico você deseja. É aqui que entra a sintaxe turbofish (::<>), que permite especificar o tipo genérico explicitamente.

Exemplo básico:

let v: Vec<i32> = (1..5).collect(); // Vec contendo [1, 2, 3, 4]

Sem a anotação de tipo, o compilador não consegue inferir o tipo de coleção. Podemos usar turbofish para especificar o tipo diretamente:

let v = (1..5).collect::<Vec<i32>>();

Outros exemplos de coleções:

use std::collections::HashSet;
let set: HashSet<i32> = (1..5).collect();
let s: String = ['a', 'b', 'c'].iter().collect();
let map: std::collections::HashMap<char, i32> = ('a'..='c').zip(1..=3).collect();

A sintaxe turbofish é especialmente útil quando a inferência de tipos não é possível, como em contextos de retorno de função ou closures complexas. Lembre-se de que collect é um método que consome o iterador, portanto só pode ser chamado uma vez.

fold, zip, enumerate

Esses três métodos são fundamentais para operações de redução, combinação e indexação de iteradores.

fold: Reduz o iterador a um único valor, aplicando uma função acumuladora a cada elemento. É equivalente ao reduce de outras linguagens, mas com um valor inicial explícito. Exemplo: somar todos os números de 1 a 100.

let sum = (1..=100).fold(0, |acc, x| acc + x);
println!("Soma: {}", sum); // 5050

O primeiro argumento é o valor inicial (0), e o closure recebe o acumulador e o elemento atual. fold é útil para operações como cálculo de média, concatenação condicional, etc.

zip: Combina dois iteradores em um único iterador de pares (a, b). O iterador resultante termina quando o menor dos dois iteradores se esgota. Exemplo:

let nomes = ["Alice", "Bob", "Carol"];
let idades = [30, 25, 28];
for (nome, idade) in nomes.iter().zip(idades.iter()) {
    println!("{} tem {} anos", nome, idade);
}

enumerate: Adiciona um contador a cada elemento do iterador, produzindo pares (índice, valor). Muito útil quando você precisa do índice durante a iteração. Exemplo:

let v = vec!["a", "b", "c"];
for (i, &item) in v.iter().enumerate() {
    println!("Posição {}: {}", i, item);
}

Combine enumerate com zip para iterar sobre múltiplas coleções com índices. Esses métodos são zero-cost abstractions: o compilador otimiza para loops manuais equivalentes.

Criando seu iterador

Para criar um iterador personalizado, você precisa implementar a trait Iterator para o seu tipo. A trait exige a definição do tipo associado Item e do método next, que retorna Option<Self::Item>. Vamos criar um iterador que gera números de Fibonacci até um limite.

struct Fibonacci {
    curr: u64,
    next: u64,
    max: u64,
}

impl Fibonacci {
    fn new(max: u64) -> Self {
        Fibonacci { curr: 0, next: 1, max }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let current = self.curr;
        if current > self.max {
            return None;
        }
        self.curr = self.next;
        self.next = current + self.next;
        Some(current)
    }
}

fn main() {
    let fib = Fibonacci::new(100);
    for num in fib {
        println!("{}", num);
    }
}

Você também pode implementar a trait IntoIterator para que seu tipo possa ser usado em loops for diretamente. Geralmente, isso é feito retornando um iterador que implementa Iterator. Além disso, é comum implementar Iterator para referências ou valores mutáveis, conforme necessário.

Lembre-se de que o método next deve ser eficiente e não realizar alocações desnecessárias. Seu iterador pode armazenar estado interno, como índices ou buffers.

Performance

Uma das maiores vantagens dos iteradores em Rust é que eles são abstrações de custo zero: o compilador é capaz de otimizar cadeias de métodos de iteradores para código tão eficiente quanto loops manuais. Isso é possível graças à monomorfização e à inlineagem. Por exemplo, uma expressão como v.iter().filter(|x| x % 2 == 0).map(|x| x * 2).collect::<Vec<_>>() é compilada para um loop otimizado sem overhead de closures ou alocações intermediárias.

No entanto, existem algumas armadilhas de performance a evitar:

  • Coleta desnecessária: Se você só precisa iterar, não colete em um Vec a menos que precise armazenar os resultados. Prefira usar for diretamente sobre o iterador.
  • Closures que capturam muitos dados: Embora closures sejam eficientes, capturar grandes estruturas por valor pode causar cópias. Use referências quando possível.
  • Encadeamento excessivo: Embora o compilador otimize bem, cadeias muito longas podem aumentar o tempo de compilação. Em código crítico, considere loops manuais se a clareza não for prejudicada.
  • Iteradores sobre slices vs. Vec: Iterar sobre um slice (&[T]) é geralmente mais rápido que sobre um Vec porque evita checagens de bounds em tempo de execução? Na verdade, ambos são seguros e otimizados; a diferença é mínima.

Para medir performance, use benchmarks com cargo bench ou ferramentas como perf. Em geral, confie nos iteradores e só otimize manualmente quando perfis indicarem gargalos.

Boas práticas

  • Prefira métodos de iterador a loops explícitos quando a lógica for clara.
  • Use collect com turbofish quando o tipo não for óbvio.
  • Implemente Iterator para seus tipos sempre que fizer sentido iterar sobre eles.
  • Aproveite métodos como filter, map, fold para expressar intenção.
  • Evite efeitos colaterais dentro de closures; use for se precisar de side effects.

Referências

Exercícios

  1. Crie um iterador personalizado chamado Step que itera de um valor inicial até um valor final (inclusive) com um passo fixo. Implemente Iterator e teste com um loop for.

    ✓ Resposta:
    struct Step {
        current: i32,
        end: i32,
        step: i32,
    }
    
    impl Step {
        fn new(start: i32, end: i32, step: i32) -> Self {
            Step { current: start, end, step }
        }
    }
    
    impl Iterator for Step {
        type Item = i32;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.current > self.end {
                return None;
            }
            let result = self.current;
            self.current += self.step;
            Some(result)
        }
    }
    
    fn main() {
        let step = Step::new(0, 10, 2);
        for val in step {
            println!("{}", val);
        }
    }
  2. Use fold para calcular o produto de todos os números de 1 a 10 que são ímpares.

    ✓ Resposta:
    let product = (1..=10).filter(|x| x % 2 != 0).fold(1, |acc, x| acc * x);
    println!("Produto: {}", product); // 945
  3. Dados dois vetores nomes e idades, use zip e collect para criar um HashMap<&str, i32>.

    ✓ Resposta:
    use std::collections::HashMap;
    
    let nomes = ["Alice", "Bob", "Carol"];
    let idades = [30, 25, 28];
    let map: HashMap<&str, i32> = nomes.iter().zip(idades.iter()).map(|(&n, &i)| (n, i)).collect();
    println!("{:?}", map);
  4. Usando enumerate, imprima cada caractere de uma string com seu índice, mas apenas se o caractere for uma vogal (a, e, i, o, u).

    ✓ Resposta:
    let s = "hello world";
    for (i, c) in s.chars().enumerate() {
        if "aeiou".contains(c) {
            println!("Índice {}: {}", i, c);
        }
    }
  5. Explique por que o código a seguir não compila e corrija-o usando turbofish: let v = (1..5).collect();

    ✓ Resposta:O compilador não consegue inferir o tipo da coleção. Correção: let v = (1..5).collect::<Vec<i32>>(); ou anotando o tipo de v: let v: Vec<i32> = (1..5).collect();