Nesta aula, vamos nos aprofundar nos iteradores em Rust, uma das ferramentas mais poderosas da linguagem. Você aprenderá a usar métodos avançados como collect, fold, zip e enumerate, além de criar seus próprios iteradores. Também discutiremos questões de performance e como os iteradores podem ser tão eficientes quanto loops manuais.

collect e turbofish

O método collect é usado para transformar um iterador em uma coleção, como um Vec, HashMap ou String. Ele consome o iterador e coleta todos os elementos em um tipo que implementa FromIterator. A sintaxe básica é:

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

Às vezes, o tipo de destino não pode ser inferido automaticamente, e precisamos usar a sintaxe turbofish ::<> para especificar o tipo genérico. Por exemplo:

let s: String = vec!['a', 'b', 'c'].iter().collect(); // erro: não pode inferir
let s = vec!['a', 'b', 'c'].iter().collect::<String>(); // correto

O turbofish também é útil com collect para coleções como HashMap:

use std::collections::HashMap;
let pairs = [(1, "um"), (2, "dois")];
let map: HashMap<_, _> = pairs.iter().cloned().collect(); // ou
let map = pairs.iter().cloned().collect::<HashMap<_, _>>();

Outra aplicação comum é coletar em Result ou Option:

let numbers = vec!["1", "2", "abc"];
let parsed: Result<Vec<i32>, _> = numbers.iter().map(|s| s.parse::<i32>()).collect();
// parsed é Err

fold, zip, enumerate

fold

fold é um método que acumula um valor ao longo do iterador. Ele recebe um valor inicial e um closure que recebe o acumulador e o elemento atual, retornando o novo acumulador. É similar a reduce, mas com um valor inicial explícito. Exemplo:

let sum = (1..5).fold(0, |acc, x| acc + x); // sum = 10

Note que fold é um consumidor: ele consome o iterador e retorna um único valor.

zip

zip combina dois iteradores em um único iterador de pares. Ele para quando o menor iterador termina. Exemplo:

let nums = [1, 2, 3];
let letras = ['a', 'b', 'c', 'd'];
for (n, l) in nums.iter().zip(letras.iter()) {
    println!("{}: {}", n, l);
}
// Imprime: 1: a, 2: b, 3: c (d é ignorado)

enumerate

enumerate adiciona um índice a cada elemento, retornando pares (índice, valor). Muito útil para loops com contagem:

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

Você pode combinar esses métodos de forma poderosa:

let v = vec![1, 2, 3, 4];
let result: Vec<_> = v.iter()
    .enumerate()
    .map(|(i, x)| (i, x * 2))
    .collect();
// result = [(0, 2), (1, 4), (2, 6), (3, 8)]

Criando seu iterador

Para criar um iterador personalizado, você precisa implementar a trait Iterator para um tipo. A trait exige um método next que retorna Option<Self::Item>. Vamos criar um iterador que gera números Fibonacci:

struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let current = self.a;
        self.a = self.b;
        self.b = current + self.b;
        Some(current) // infinito, nunca retorna None
    }
}

fn main() {
    let fib = Fibonacci::new();
    for (i, n) in fib.take(10).enumerate() {
        println!("Fib {}: {}", i, n);
    }
}

Você também pode criar iteradores que retornam None para sinalizar o fim. Por exemplo, um iterador que percorre um intervalo decrescente:

struct Countdown {
    current: i32,
}

impl Countdown {
    fn new(start: i32) -> Self {
        Countdown { current: start }
    }
}

impl Iterator for Countdown {
    type Item = i32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < 0 {
            None
        } else {
            let val = self.current;
            self.current -= 1;
            Some(val)
        }
    }
}

fn main() {
    let count = Countdown::new(5);
    for n in count {
        println!("{}", n);
    }
}

Lembre-se de que iteradores podem ser compostos com métodos como map, filter, etc., desde que implementem Iterator.

Performance

Iteradores em Rust são projetados para serem tão eficientes quanto loops manuais, graças ao sistema de tipos e à eliminação de custos abstratos (zero-cost abstractions). O compilador é capaz de otimizar cadeias de métodos de iteradores, muitas vezes gerando código idêntico ao de um loop escrito à mão.

Por exemplo, o seguinte código com iteradores:

let v: Vec<i32> = (0..100).filter(|x| x % 2 == 0).map(|x| x * 2).collect();

é compilado para um loop eficiente, sem alocações desnecessárias ou chamadas de função virtuais. No entanto, é importante estar ciente de algumas armadilhas:

  • Coleta prematura: Evite coletar em um Vec se você só precisa iterar uma vez. Prefira encadear métodos e consumir no final.
  • Clone desnecessário: Use cloned() ou copied() com cuidado para evitar cópias caras.
  • Alocações: Métodos como collect alocam memória. Se você estiver em um contexto crítico de performance, considere usar fold ou loops.
  • Iteradores sobre slices: Iterar sobre slices (&[T]) é rápido, pois são referências.

Uma dica: use o método size_hint para ajudar o compilador a otimizar alocações. Muitos iteradores implementam size_hint para dar uma estimativa do tamanho. Você também pode usar Iterator::size_hint em seus próprios iteradores.

Boas práticas

  • Prefira métodos de iteradores a loops explícitos quando a lógica for clara e concisa.
  • Use collect com turbofish quando o tipo não puder ser inferido.
  • Ao criar iteradores, implemente Iterator e, se aplicável, IntoIterator para que seu tipo possa ser usado em loops for.
  • Documente seu iterador, especialmente se ele for infinito ou tiver comportamento não óbvio.
  • Teste iteradores com take para limitar a saída em testes.

Referências

Exercícios

  1. Crie um iterador personalizado chamado Step que começa em um valor inicial e incrementa por um passo fixo, parando quando ultrapassar um limite. Por exemplo, Step::new(0, 2, 10) deve gerar 0, 2, 4, 6, 8.

    ✓ Resposta:
    struct Step {
        current: i32,
        step: i32,
        limit: i32,
    }
    
    impl Step {
        fn new(start: i32, step: i32, limit: i32) -> Self {
            Step { current: start, step, limit }
        }
    }
    
    impl Iterator for Step {
        type Item = i32;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.current >= self.limit {
                None
            } else {
                let val = self.current;
                self.current += self.step;
                Some(val)
            }
        }
    }
    
    fn main() {
        let step = Step::new(0, 2, 10);
        for n in step {
            println!("{}", n);
        }
    }
  2. Use fold para calcular o fatorial de um número. Por exemplo, fatorial(5) = 120.

    ✓ Resposta:
    fn factorial(n: u64) -> u64 {
        (1..=n).fold(1, |acc, x| acc * x)
    }
    
    fn main() {
        println!("{}", factorial(5)); // 120
    }
  3. Dado um vetor de strings, use zip e enumerate para criar um novo vetor de tuplas contendo o índice, a string e o comprimento da string. Exemplo: ["a", "ab", "abc"] vira [(0, "a", 1), (1, "ab", 2), (2, "abc", 3)].

    ✓ Resposta:
    fn main() {
        let v = vec!["a", "ab", "abc"];
        let result: Vec<(usize, &str, usize)> = v.iter()
            .enumerate()
            .map(|(i, s)| (i, *s, s.len()))
            .collect();
        println!("{:?}", result);
    }
  4. Crie um iterador que gere números primos infinitamente (use o crivo de Eratóstenes ou um método simples). Use take para obter os 10 primeiros.

    ✓ Resposta:
    struct Primes {
        current: u64,
    }
    
    impl Primes {
        fn new() -> Self {
            Primes { current: 2 }
        }
    
        fn is_prime(n: u64) -> bool {
            if n < 2 { return false; }
            let limit = (n as f64).sqrt() as u64;
            (2..=limit).all(|i| n % i != 0)
        }
    }
    
    impl Iterator for Primes {
        type Item = u64;
    
        fn next(&mut self) -> Option<Self::Item> {
            while !Self::is_prime(self.current) {
                self.current += 1;
            }
            let prime = self.current;
            self.current += 1;
            Some(prime)
        }
    }
    
    fn main() {
        let primes = Primes::new();
        for p in primes.take(10) {
            println!("{}", p);
        }
    }
  5. Explique por que o seguinte código não compila e corrija-o usando turbofish: let v = (1..5).collect();

    ✓ Resposta:

    O código não compila porque o compilador não consegue inferir o tipo de coleção para o collect. É necessário especificar o tipo, por exemplo:

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