Iteradores são o coração da programação funcional em Rust. Eles permitem processar sequências de dados de forma expressiva, componível e — graças às zero-cost abstractions — sem nenhum custo de performance em relação a loops manuais. Neste artigo, vamos explorar o trait Iterator em profundidade, dominar os combinadores mais importantes e criar iteradores customizados.
O Trait Iterator
No núcleo de tudo está o trait Iterator, surpreendentemente simples:
// Definição simplificada da biblioteca padrão
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// + dezenas de métodos com implementação padrão
// (map, filter, fold, collect, etc.)
}
Todo iterador precisa implementar apenas um método: next(). Todos os outros são derivados automaticamente.
Exemplo Básico
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
let mut iter = numeros.iter(); // cria um iterador
// next() retorna Option<&i32>
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // Some(4)
println!("{:?}", iter.next()); // Some(5)
println!("{:?}", iter.next()); // None — acabou
}
Três Formas de Criar Iteradores
fn main() {
let dados = vec![String::from("a"), String::from("b"), String::from("c")];
// .iter() → Iterator<Item = &T>
// Não consome o vetor — empréstimo imutável
for item in dados.iter() {
println!("Referência: {}", item);
}
println!("Dados ainda acessíveis: {:?}", dados);
// .iter_mut() → Iterator<Item = &mut T>
// Não consome, mas permite mutação
let mut dados_mut = dados.clone();
for item in dados_mut.iter_mut() {
item.push_str("!");
}
println!("Mutados: {:?}", dados_mut);
// .into_iter() → Iterator<Item = T>
// Consome o vetor — toma ownership
for item in dados.into_iter() {
println!("Owned: {}", item);
}
// println!("{:?}", dados); // ERRO: dados foi consumido
}
Lazy Evaluation: Iteradores São Preguiçosos
Uma característica fundamental: adaptadores de iteradores não fazem nada até serem consumidos:
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
// Isso NÃO executa nada — apenas cria uma cadeia de adaptadores
let _cadeia = numeros.iter()
.map(|x| {
println!("map: {}", x); // nunca é impresso!
x * 2
})
.filter(|x| {
println!("filter: {}", x); // nunca é impresso!
*x > 4
});
println!("Nada aconteceu ainda...");
// Agora sim — collect() consome o iterador
let resultado: Vec<i32> = numeros.iter()
.map(|x| {
println!("map: {}", x);
x * 2
})
.filter(|x| {
println!("filter: {}", x);
*x > 4
})
.collect();
println!("Resultado: {:?}", resultado);
}
Isso é eficiente porque cada elemento é processado um por vez pela cadeia inteira, sem criar coleções intermediárias:
Processamento elemento a elemento:
─────────────────────────────────
1 → map(2) → filter(2 > 4? Não) → descartado
2 → map(4) → filter(4 > 4? Não) → descartado
3 → map(6) → filter(6 > 4? Sim) → resultado
4 → map(8) → filter(8 > 4? Sim) → resultado
5 → map(10) → filter(10 > 4? Sim) → resultado
Sem coleções intermediárias!
Combinadores Essenciais
map — Transformar cada elemento
fn main() {
let nomes = vec!["ana", "bruno", "clara"];
let maiusculos: Vec<String> = nomes.iter()
.map(|nome| nome.to_uppercase())
.collect();
println!("{:?}", maiusculos); // ["ANA", "BRUNO", "CLARA"]
}
filter — Selecionar elementos
fn main() {
let numeros: Vec<i32> = (1..=20).collect();
let pares: Vec<&i32> = numeros.iter()
.filter(|n| *n % 2 == 0)
.collect();
println!("Pares: {:?}", pares); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
}
filter_map — Filtrar e transformar ao mesmo tempo
fn main() {
let textos = vec!["42", "abc", "7", "xyz", "100"];
let numeros: Vec<i32> = textos.iter()
.filter_map(|t| t.parse::<i32>().ok())
.collect();
println!("{:?}", numeros); // [42, 7, 100]
}
fold — Acumular em um resultado
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
// Soma com fold
let soma = numeros.iter().fold(0, |acc, x| acc + x);
println!("Soma: {}", soma); // 15
// Construir string com fold
let texto = numeros.iter().fold(String::new(), |mut acc, x| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(&x.to_string());
acc
});
println!("Texto: {}", texto); // 1, 2, 3, 4, 5
// Encontrar máximo e mínimo simultaneamente
let (min, max) = numeros.iter().fold((i32::MAX, i32::MIN), |(min, max), &x| {
(min.min(x), max.max(x))
});
println!("Min: {}, Max: {}", min, max); // Min: 1, Max: 5
}
flat_map — Achatar iteradores aninhados
fn main() {
let frases = vec!["olá mundo", "rust é incrível", "iteradores são poderosos"];
let palavras: Vec<&str> = frases.iter()
.flat_map(|frase| frase.split_whitespace())
.collect();
println!("{:?}", palavras);
// ["olá", "mundo", "rust", "é", "incrível", "iteradores", "são", "poderosos"]
}
enumerate — Adicionar índice
fn main() {
let frutas = vec!["maçã", "banana", "cereja"];
for (i, fruta) in frutas.iter().enumerate() {
println!("{}. {}", i + 1, fruta);
}
}
zip — Combinar dois iteradores
fn main() {
let nomes = vec!["Ana", "Bruno", "Clara"];
let idades = vec![25, 30, 28];
let pessoas: Vec<String> = nomes.iter()
.zip(idades.iter())
.map(|(nome, idade)| format!("{} ({} anos)", nome, idade))
.collect();
println!("{:?}", pessoas);
// ["Ana (25 anos)", "Bruno (30 anos)", "Clara (28 anos)"]
}
chain — Concatenar iteradores
fn main() {
let primeira_parte = vec![1, 2, 3];
let segunda_parte = vec![4, 5, 6];
let todos: Vec<i32> = primeira_parte.iter()
.chain(segunda_parte.iter())
.copied()
.collect();
println!("{:?}", todos); // [1, 2, 3, 4, 5, 6]
}
take, skip, take_while, skip_while
fn main() {
let numeros: Vec<i32> = (1..=10).collect();
let primeiros_3: Vec<&i32> = numeros.iter().take(3).collect();
println!("Primeiros 3: {:?}", primeiros_3); // [1, 2, 3]
let apos_3: Vec<&i32> = numeros.iter().skip(3).collect();
println!("Após 3: {:?}", apos_3); // [4, 5, 6, 7, 8, 9, 10]
let enquanto_menor_5: Vec<&i32> = numeros.iter()
.take_while(|&&x| x < 5)
.collect();
println!("Enquanto < 5: {:?}", enquanto_menor_5); // [1, 2, 3, 4]
let apos_menor_5: Vec<&i32> = numeros.iter()
.skip_while(|&&x| x < 5)
.collect();
println!("Após < 5: {:?}", apos_menor_5); // [5, 6, 7, 8, 9, 10]
}
Consumidores: sum, product, min, max, count
fn main() {
let numeros = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
println!("Soma: {}", numeros.iter().sum::<i32>()); // 44
println!("Produto: {}", numeros.iter().product::<i32>()); // 48600
println!("Mínimo: {:?}", numeros.iter().min()); // Some(1)
println!("Máximo: {:?}", numeros.iter().max()); // Some(9)
println!("Contagem: {}", numeros.iter().count()); // 11
// any e all
println!("Algum > 8? {}", numeros.iter().any(|&x| x > 8)); // true
println!("Todos > 0? {}", numeros.iter().all(|&x| x > 0)); // true
println!("Todos > 5? {}", numeros.iter().all(|&x| x > 5)); // false
// find e position
println!("Primeiro > 5: {:?}", numeros.iter().find(|&&x| x > 5)); // Some(9)
println!("Posição > 5: {:?}", numeros.iter().position(|&x| x > 5)); // Some(5)
}
collect — O Canivete Suíço
collect() é incrivelmente versátil — o tipo de retorno determina o que ele faz:
use std::collections::{HashMap, HashSet, BTreeSet};
fn main() {
let dados = vec![("Ana", 25), ("Bruno", 30), ("Ana", 28)];
// Coletar em Vec
let nomes: Vec<&str> = dados.iter().map(|(nome, _)| *nome).collect();
println!("Vec: {:?}", nomes);
// Coletar em HashSet (remove duplicatas)
let unicos: HashSet<&str> = dados.iter().map(|(nome, _)| *nome).collect();
println!("HashSet: {:?}", unicos);
// Coletar em BTreeSet (remove duplicatas + ordena)
let ordenados: BTreeSet<&str> = dados.iter().map(|(nome, _)| *nome).collect();
println!("BTreeSet: {:?}", ordenados);
// Coletar em HashMap
let mapa: HashMap<&str, i32> = dados.into_iter().collect();
println!("HashMap: {:?}", mapa);
// Coletar Result em Result<Vec, Error>
let textos = vec!["1", "2", "abc", "4"];
let resultado: Result<Vec<i32>, _> = textos.iter()
.map(|t| t.parse::<i32>())
.collect();
println!("Result: {:?}", resultado); // Err(ParseIntError)
// Coletar em String
let chars = vec!['R', 'u', 's', 't'];
let texto: String = chars.into_iter().collect();
println!("String: {}", texto); // Rust
}
Criando Iteradores Customizados
Iterador Simples: Sequência de Fibonacci
struct Fibonacci {
a: u64,
b: u64,
}
impl Fibonacci {
fn novo() -> Self {
Fibonacci { a: 0, b: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let resultado = self.a;
let novo_b = self.a + self.b;
self.a = self.b;
self.b = novo_b;
Some(resultado) // iterador infinito
}
}
fn main() {
// Fibonacci é preguiçoso — podemos pegar apenas o que queremos
let primeiros_10: Vec<u64> = Fibonacci::novo().take(10).collect();
println!("Primeiros 10: {:?}", primeiros_10);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Primeiro Fibonacci maior que 1000
let grande = Fibonacci::novo()
.find(|&n| n > 1000)
.unwrap();
println!("Primeiro > 1000: {}", grande); // 1597
// Soma dos Fibonacci menores que 100
let soma: u64 = Fibonacci::novo()
.take_while(|&n| n < 100)
.sum();
println!("Soma dos < 100: {}", soma); // 232
}
Iterador com Estado: Janela Deslizante
struct JanelaDeslizante<'a, T> {
dados: &'a [T],
tamanho: usize,
posicao: usize,
}
impl<'a, T> JanelaDeslizante<'a, T> {
fn nova(dados: &'a [T], tamanho: usize) -> Self {
JanelaDeslizante {
dados,
tamanho,
posicao: 0,
}
}
}
impl<'a, T> Iterator for JanelaDeslizante<'a, T> {
type Item = &'a [T];
fn next(&mut self) -> Option<Self::Item> {
if self.posicao + self.tamanho <= self.dados.len() {
let janela = &self.dados[self.posicao..self.posicao + self.tamanho];
self.posicao += 1;
Some(janela)
} else {
None
}
}
// Otimização: implementar size_hint para collect() eficiente
fn size_hint(&self) -> (usize, Option<usize>) {
let restante = if self.dados.len() >= self.posicao + self.tamanho {
self.dados.len() - self.posicao - self.tamanho + 1
} else {
0
};
(restante, Some(restante))
}
}
fn main() {
let dados = vec![1, 2, 3, 4, 5, 6, 7];
// Média móvel com janela de 3
let medias: Vec<f64> = JanelaDeslizante::nova(&dados, 3)
.map(|janela| {
let soma: i32 = janela.iter().sum();
soma as f64 / janela.len() as f64
})
.collect();
println!("Médias móveis: {:?}", medias);
// [2.0, 3.0, 4.0, 5.0, 6.0]
// Nota: na prática, use .windows(3) que já existe nos slices
let medias2: Vec<f64> = dados.windows(3)
.map(|w| w.iter().sum::<i32>() as f64 / w.len() as f64)
.collect();
assert_eq!(medias, medias2);
}
Implementando IntoIterator para Tipos Próprios
#[derive(Debug)]
struct Matriz {
dados: Vec<Vec<f64>>,
linhas: usize,
colunas: usize,
}
impl Matriz {
fn nova(linhas: usize, colunas: usize) -> Self {
Matriz {
dados: vec![vec![0.0; colunas]; linhas],
linhas,
colunas,
}
}
fn definir(&mut self, linha: usize, coluna: usize, valor: f64) {
self.dados[linha][coluna] = valor;
}
}
// Iterador sobre elementos da matriz (linha por linha)
struct MatrizIter<'a> {
matriz: &'a Matriz,
linha: usize,
coluna: usize,
}
impl<'a> Iterator for MatrizIter<'a> {
type Item = (usize, usize, f64);
fn next(&mut self) -> Option<Self::Item> {
if self.linha >= self.matriz.linhas {
return None;
}
let resultado = (
self.linha,
self.coluna,
self.matriz.dados[self.linha][self.coluna],
);
self.coluna += 1;
if self.coluna >= self.matriz.colunas {
self.coluna = 0;
self.linha += 1;
}
Some(resultado)
}
}
impl<'a> IntoIterator for &'a Matriz {
type Item = (usize, usize, f64);
type IntoIter = MatrizIter<'a>;
fn into_iter(self) -> Self::IntoIter {
MatrizIter {
matriz: self,
linha: 0,
coluna: 0,
}
}
}
fn main() {
let mut m = Matriz::nova(2, 3);
m.definir(0, 0, 1.0);
m.definir(0, 1, 2.0);
m.definir(0, 2, 3.0);
m.definir(1, 0, 4.0);
m.definir(1, 1, 5.0);
m.definir(1, 2, 6.0);
// Agora podemos iterar com for
for (l, c, v) in &m {
println!("[{},{}] = {}", l, c, v);
}
// E usar combinadores
let soma: f64 = m.into_iter().map(|(_, _, v)| v).sum();
println!("Soma: {}", soma); // 21
}
Zero-Cost Abstractions: A Prova
Iteradores em Rust são compilados para código tão eficiente quanto loops manuais. Veja a equivalência:
fn soma_pares_imperativo(dados: &[i32]) -> i32 {
let mut soma = 0;
for i in 0..dados.len() {
if dados[i] % 2 == 0 {
soma += dados[i] * dados[i];
}
}
soma
}
fn soma_pares_funcional(dados: &[i32]) -> i32 {
dados.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * x)
.sum()
}
fn main() {
let dados: Vec<i32> = (1..=100).collect();
let r1 = soma_pares_imperativo(&dados);
let r2 = soma_pares_funcional(&dados);
assert_eq!(r1, r2);
println!("Ambos dão: {}", r1); // 171700
}
O compilador Rust gera assembly idêntico para ambas as versões (com otimizações ligadas). Isso é possível porque:
- Iteradores são structs com tamanho conhecido em tempo de compilação
- O método
next()é inlined - Não há alocação no heap
- Não há dispatch dinâmico
Erros Comuns
1. Esquecer de consumir o iterador
fn main() {
let dados = vec![1, 2, 3];
// AVISO: iterador não consumido — não faz nada!
dados.iter().map(|x| println!("{}", x));
// CORRETO: use for_each() para efeitos colaterais
dados.iter().for_each(|x| println!("{}", x));
// Ou use for
for x in &dados {
println!("{}", x);
}
}
2. Tentar iterar duas vezes sobre um iterador consumido
fn main() {
let dados = vec![1, 2, 3];
let iter = dados.into_iter(); // consome dados
let soma: i32 = iter.sum(); // consome iter
// let produto: i32 = iter.product(); // ERRO: iter já foi consumido
// SOLUÇÃO: crie iteradores separados
let dados = vec![1, 2, 3];
let soma: i32 = dados.iter().sum();
let produto: i32 = dados.iter().product();
println!("Soma: {}, Produto: {}", soma, produto);
}
Aplicação no Mundo Real: Processamento de Dados
use std::collections::HashMap;
#[derive(Debug)]
struct Venda {
produto: String,
categoria: String,
valor: f64,
quantidade: u32,
}
fn main() {
let vendas = vec![
Venda { produto: "Notebook".into(), categoria: "Eletrônicos".into(), valor: 3500.0, quantidade: 2 },
Venda { produto: "Mouse".into(), categoria: "Eletrônicos".into(), valor: 89.90, quantidade: 15 },
Venda { produto: "Cadeira".into(), categoria: "Móveis".into(), valor: 899.0, quantidade: 3 },
Venda { produto: "Teclado".into(), categoria: "Eletrônicos".into(), valor: 199.90, quantidade: 8 },
Venda { produto: "Mesa".into(), categoria: "Móveis".into(), valor: 1200.0, quantidade: 2 },
Venda { produto: "Monitor".into(), categoria: "Eletrônicos".into(), valor: 1800.0, quantidade: 4 },
];
// Total por categoria
let total_por_categoria: HashMap<&str, f64> = vendas.iter()
.map(|v| (v.categoria.as_str(), v.valor * v.quantidade as f64))
.fold(HashMap::new(), |mut acc, (cat, total)| {
*acc.entry(cat).or_insert(0.0) += total;
acc
});
println!("Total por categoria:");
for (cat, total) in &total_por_categoria {
println!(" {}: R${:.2}", cat, total);
}
// Produto com maior faturamento
let top_produto = vendas.iter()
.max_by(|a, b| {
let fa = a.valor * a.quantidade as f64;
let fb = b.valor * b.quantidade as f64;
fa.partial_cmp(&fb).unwrap()
})
.unwrap();
println!("\nTop produto: {} (R${:.2})",
top_produto.produto,
top_produto.valor * top_produto.quantidade as f64
);
// Produtos eletrônicos ordenados por valor
let mut eletronicos: Vec<&Venda> = vendas.iter()
.filter(|v| v.categoria == "Eletrônicos")
.collect();
eletronicos.sort_by(|a, b| b.valor.partial_cmp(&a.valor).unwrap());
println!("\nEletrônicos (por valor):");
for v in eletronicos {
println!(" {} - R${:.2}", v.produto, v.valor);
}
}
Veja Também
- Como Iterar Coleções em Rust — receita prática de iteração
- Como Filtrar Vetor em Rust — filter, retain e filter_map
- Closures em Rust — closures são a base dos combinadores
- Pattern Matching Avançado — destructuring em iteradores
- Traits e Generics em Rust — como o trait Iterator funciona