Iteradores avançado
Esta aula aprofunda o uso de iteradores em Rust, abordando métodos avançados como collect com turbofish, fold, zip e enumerate, além de ensinar como criar iteradores personalizados e discutir considerações de performance. O conteúdo inclui exemplos práticos e exercícios para fixação.
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); // 5050O 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
Veca menos que precise armazenar os resultados. Prefira usarfordiretamente 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 umVecporque 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
collectcom turbofish quando o tipo não for óbvio. - Implemente
Iteratorpara seus tipos sempre que fizer sentido iterar sobre eles. - Aproveite métodos como
filter,map,foldpara expressar intenção. - Evite efeitos colaterais dentro de closures; use
forse precisar de side effects.
Referências
- Documentação oficial da trait Iterator
- Capítulo do livro sobre iteradores
- Rust by Example: Iterators
- Módulo std::iter
- The Rustonomicon: Performance
- Trait IntoIterator
Exercícios
Crie um iterador personalizado chamado
Stepque itera de um valor inicial até um valor final (inclusive) com um passo fixo. ImplementeIteratore teste com um loopfor.✓ 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); } }Use
foldpara 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); // 945Dados dois vetores
nomeseidades, usezipecollectpara criar umHashMap<&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);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); } }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 dev:let v: Vec<i32> = (1..5).collect();