Iteradores
Esta aula cobre o trait Iterator em Rust, o método next, adaptadores como map e filter, e o conceito de lazy evaluation. Você aprenderá a criar e combinar iteradores de forma eficiente e idiomática.
Iteradores são um dos pilares da programação funcional em Rust. Eles permitem processar sequências de elementos de forma declarativa, segura e eficiente. Nesta aula, exploraremos o trait Iterator, seu método fundamental next, os adaptadores mais comuns (map, filter) e o conceito de avaliação preguiçosa (lazy evaluation), que torna os iteradores tão performáticos.
Dominar iteradores é essencial para escrever código Rust idiomático e de alto desempenho. A biblioteca padrão faz uso intenso de iteradores, e você os encontrará em manipulação de coleções, I/O, parsing e muito mais.
trait Iterator
O trait Iterator é definido na biblioteca padrão e fornece uma interface para tipos que podem produzir uma sequência de valores. Ele exige a implementação de um único método: next, que retorna Option<Self::Item>. Quando o iterador termina, next retorna None.
Implementar o trait Iterator manualmente permite criar iteradores personalizados. Por exemplo, um contador que gera números de 1 a N:
struct Contador {
max: u32,
atual: u32,
}
impl Contador {
fn new(max: u32) -> Self {
Contador { max, atual: 0 }
}
}
impl Iterator for Contador {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.atual += 1;
if self.atual <= self.max {
Some(self.atual)
} else {
None
}
}
}
fn main() {
let mut c = Contador::new(5);
while let Some(val) = c.next() {
println!("{}", val);
}
}O trait Iterator também fornece dezenas de métodos padrão (chamados adaptadores) que são implementados automaticamente para qualquer tipo que implemente next. Isso inclui map, filter, fold, collect, entre outros.
next
O método next é o coração de todo iterador. Ele avança o iterador e retorna o próximo elemento, ou None se a iteração terminou. Chamar next repetidamente permite percorrer a sequência manualmente, mas geralmente usamos loops for ou adaptadores que chamam next internamente.
O loop for em Rust é syntactic sugar para uma chamada repetida a next até que retorne None. Por exemplo:
let v = vec![1, 2, 3];
for i in v.iter() {
println!("{}", i);
}
// Equivalente a:
let mut it = v.iter();
while let Some(i) = it.next() {
println!("{}", i);
}Note que v.iter() retorna um iterador sobre referências (&i32). Existem também iter_mut() para referências mutáveis e into_iter() para iteradores que consomem o valor (movendo os elementos).
Adaptadores (map, filter)
Adaptadores são métodos do trait Iterator que transformam um iterador em outro iterador. Eles são lazy, ou seja, não executam a transformação até que um consumidor (como collect ou for) force a iteração.
map: aplica uma função a cada elemento, produzindo um novo iterador com os resultados.
let v = vec![1, 2, 3];
let quadrados: Vec<i32> = v.iter().map(|x| x * x).collect();
println!("{:?}", quadrados); // [1, 4, 9]filter: seleciona elementos que satisfazem um predicado (função que retorna bool).
let v = vec![1, 2, 3, 4, 5];
let pares: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", pares); // [2, 4]Adaptadores podem ser encadeados para criar pipelines complexas de forma legível:
let v = vec![1, 2, 3, 4, 5];
let resultado: Vec<i32> = v.iter()
.filter(|&&x| x > 2)
.map(|x| x * 10)
.collect();
println!("{:?}", resultado); // [30, 40, 50]Outros adaptadores comuns incluem take, skip, enumerate, zip, chain, flat_map, etc.
Lazy evaluation
Lazy evaluation (avaliação preguiçosa) significa que as transformações em um iterador só são executadas quando necessário. Em Rust, os adaptadores de iterador são lazy por padrão. Isso permite construir pipelines complexas sem custo até o momento do consumo.
Por exemplo, o código a seguir não imprime nada, porque map é lazy e collect não é chamado:
let v = vec![1, 2, 3];
v.iter().map(|x| {
println!("processando {}", x);
x * 2
});
// Nada é impressoPara forçar a execução, precisamos consumir o iterador com collect, for_each, ou um loop for:
let v = vec![1, 2, 3];
let resultado: Vec<i32> = v.iter().map(|x| {
println!("processando {}", x);
x * 2
}).collect();
// Imprime:
// processando 1
// processando 2
// processando 3Lazy evaluation traz vantagens de performance: se você usar take(2) em um iterador infinito, apenas dois elementos são processados. Também permite trabalhar com sequências potencialmente infinitas, como números aleatórios ou leitura de um arquivo.
Entretanto, é preciso cuidado com efeitos colaterais: closures dentro de adaptadores lazy podem não ser executadas se o iterador não for consumido. Por isso, é idiomático manter as closures sem efeitos colaterais (puramente funcionais) ou usar for_each quando efeitos são necessários.
Boas práticas:
- Prefira encadear adaptadores a escrever loops explícitos – o código fica mais declarativo e menos propenso a erros.
- Use
collectpara materializar o resultado em um vetor, mas lembre-se de que isso aloca memória. Para operações curtas, pode ser aceitável; para grandes volumes, considere processar elemento a elemento. - Evite closures que capturem muitas variáveis ou tenham efeitos colaterais – isso pode dificultar a compreensão e prejudicar a lazy evaluation.
Referências
- Documentação oficial do trait Iterator
- Capítulo 13 do Livro de Rust: Iteradores
- Rust by Example: Iterators
- Módulo std::iter
- Blog da Rust: Traits (inclui Iterator)
Exercícios
Crie um iterador personalizado chamado
Fibonaccique gere os números da sequência de Fibonacci infinitamente. Implemente o traitIteratorpara ele. Dica: use dois camposaebpara os dois últimos valores.✓ Resposta: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 next = self.a; self.a = self.b; self.b = next + self.b; Some(next) } } fn main() { let fib = Fibonacci::new(); for (i, val) in fib.take(10).enumerate() { println!("Fib[{}] = {}", i, val); } }Usando adaptadores, escreva uma expressão que, dada uma slice de inteiros, retorne um vetor contendo apenas os números ímpares, multiplicados por 3.
✓ Resposta:let numeros = [1, 2, 3, 4, 5, 6]; let resultado: Vec<i32> = numeros.iter() .filter(|&&x| x % 2 != 0) .map(|&x| x * 3) .collect(); println!("{:?}", resultado); // [3, 9, 15]Explique por que o código a seguir não imprime nada e corrija-o para que imprima os números de 1 a 5.
(1..=5).map(|x| println!("{}", x));✓ Resposta:O
mapé lazy; a closure só é executada quando o iterador é consumido. Para imprimir, usefor_eachou um loopfor:(1..=5).for_each(|x| println!("{}", x)); // Ou: for x in 1..=5 { println!("{}", x); }Dado um vetor de strings, use adaptadores para concatenar todas as strings em uma única string, separadas por vírgula. Exemplo:
["a", "b", "c"]→"a,b,c".✓ Resposta:let palavras = vec!["a", "b", "c"]; let resultado = palavras.iter() .map(|s| s.as_str()) .collect::<Vec<&str>>() .join(","); println!("{}", resultado); // "a,b,c" // Ou mais diretamente: let resultado2 = palavras.join(","); println!("{}", resultado2); // "a,b,c"Implemente um iterador
MeuIteradorsobre uma structPessoaque tenha camposnomeeidade. O iterador deve retornar apenas os nomes das pessoas com idade maior que 18. Use um vetor de pessoas e, em seguida, usefilter_mappara obter um iterador de&str.✓ Resposta:struct Pessoa { nome: String, idade: u8, } fn main() { let pessoas = vec![ Pessoa { nome: "Alice".to_string(), idade: 25 }, Pessoa { nome: "Bob".to_string(), idade: 17 }, Pessoa { nome: "Carol".to_string(), idade: 30 }, ]; let adultos: Vec<&str> = pessoas.iter() .filter_map(|p| { if p.idade > 18 { Some(p.nome.as_str()) } else { None } }) .collect(); println!("{:?}", adultos); // ["Alice", "Carol"] }