Iterator e closure em Rust
1. Visão geral
Closure e iterator são duas ferramentas que ajudam você a processar dados de forma funcional em Rust enquanto mantém o desempenho equivalente a um loop escrito manualmente. Este artigo explica como closure captura variáveis do escopo ao redor e os três traits Fn, FnMut, FnOnce. Depois passa pelo trait Iterator, seus adaptadores comuns e como escrever seu próprio iterator. Após ler, você conseguirá escrever pipelines como filter → map → collect com confiança. Você também entenderá por que o compilador às vezes rejeita seu código.
Todos os exemplos usam Rust 1.80+ (edição 2021), apenas com a biblioteca padrão.
2. Closure básico
2.1. Sintaxe
Closure é uma função anônima que pode “enxergar” variáveis do escopo ao redor:
fn main() {
let discount = 10; // desconto em percentual
let apply_discount = |price: u32| price - price * discount / 100;
println!("{}", apply_discount(250));
println!("{}", apply_discount(1000));
}
225
900
Os parâmetros ficam entre as duas barras |...|, e o corpo da closure vem logo depois. A closure usa a variável discount mesmo sem ela ser passada como parâmetro. Uma função fn comum não conseguiria fazer isso.
Os tipos de parâmetro e o tipo de retorno geralmente são inferidos automaticamente. Você pode escrever |price| ... se o compilador tiver informação suficiente. No entanto, o tipo é inferido apenas uma vez. Se você chamar a closure com u32 pela primeira vez e depois com i64, terá erro de compilação.
2.2. Três formas de capturar variáveis
A closure captura variáveis da forma “mais leve” que o corpo da closure precisa. A ordem de prioridade é empréstimo imutável, depois empréstimo mutável, e por fim a transferência de propriedade.
fn main() {
let mut cart = vec!["caneta"];
let print_cart = || println!("Carrinho: {:?}", cart); // empréstimo &cart
print_cart();
let mut add_item = |item| cart.push(item); // empréstimo &mut cart
add_item("caderno");
add_item("régua");
println!("No final: {:?}", cart);
}
Carrinho: ["caneta"]
No final: ["caneta", "caderno", "régua"]
Analisando cada closure:
print_cartapenas lêcart, então mantém um&cart.add_itemchamapush, então precisa de&mut cart. A variável closure também deve ser declaradamut, pois chamá-la altera o estado interno.- O código compila graças aos non-lexical lifetimes.
print_cartnão é mais usada após a primeira chamada, então o empréstimo imutável termina antes do empréstimo mutável começar.
Quando você precisa que a closure possua os dados, adicione a palavra-chave move. O caso típico é passar a closure para uma thread diferente:
use std::thread;
fn main() {
let customer = String::from("Lan");
let handle = thread::spawn(move || {
println!("Processando pedido para {customer}");
});
handle.join().unwrap();
}
Processando pedido para Lan
Sem move, o compilador reporta erro. A nova thread pode viver mais tempo que main, então uma referência a customer não seria segura. Com move, a String é transferida para dentro da closure. Depois dessa linha, você não pode usar customer em main novamente.
3. Os três traits Fn, FnMut e FnOnce
Cada closure implementa automaticamente um ou mais dos três traits a seguir. Qual trait é implementado depende do que a closure faz com as variáveis capturadas:
| Trait | O que a closure faz com variáveis capturadas | Pode ser chamada quantas vezes |
|---|---|---|
Fn |
Apenas lê | Muitas vezes |
FnMut |
Modifica | Muitas vezes |
FnOnce |
Consome (move para fora) | Uma vez |
Estes três traits têm uma relação de herança: todo Fn é também FnMut, e todo FnMut é também FnOnce. Ao escrever uma função que recebe uma closure, você deve escolher o trait mais permissivo que a função precisa. Assim o chamador tem as mais opções possíveis.
fn run_twice<F: Fn()>(f: F) {
f();
f();
}
fn run_twice_mut<F: FnMut()>(mut f: F) {
f();
f();
}
fn run_once<F: FnOnce() -> String>(f: F) -> String {
f()
}
fn main() {
let name = String::from("Minh");
run_twice(|| println!("Olá {name}"));
let mut count = 0;
run_twice_mut(|| count += 1);
println!("count = {count}");
let receipt = run_once(move || name + " - pagamento realizado");
println!("{receipt}");
}
Olá Minh
Olá Minh
count = 2
Minh - pagamento realizado
As três chamadas correspondem aos três traits:
- A primeira closure apenas lê
name, então éFn. - A segunda closure incrementa
count, então éFnMut. Passá-la pararun_twiceresultaria em erro. - A última closure usa
name + "...". O operador+emStringconsomename, então a closure só pode ser chamada uma vez e implementa apenasFnOnce.
3.1. Retornando closure de uma função
Cada closure tem seu próprio tipo, anônimo. Por isso você não consegue escrever o nome desse tipo. Existem duas formas de retornar uma closure:
fn make_discount(percent: u32) -> impl Fn(u32) -> u32 {
move |price| price - price * percent / 100
}
fn pick_rule(is_vip: bool) -> Box<dyn Fn(u32) -> u32> {
if is_vip {
Box::new(|price| price * 80 / 100)
} else {
Box::new(|price| price)
}
}
fn main() {
let summer_sale = make_discount(15);
println!("{}", summer_sale(200));
let rule = pick_rule(true);
println!("{}", rule(500));
}
170
400
impl Fn(...)é usado quando a função sempre retorna um tipo de closure fixo. Essa abordagem não aloca heap e não tem overhead de chamada dinâmica.Box<dyn Fn(...)>é usado quando diferentes ramificações retornam closures diferentes. As duas closures empick_ruletêm tipos diferentes, então devem ser colocadas em um trait object comum.moveemmake_discounté obrigatório. Se faltasse, a closure pegaria emprestadapercent, que desapareceria quando a função terminasse.
4. Trait Iterator
4.1. O núcleo: a função next
Todo iterator em Rust gira em torno de um trait muito pequeno:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... dezenas de métodos prontos como map, filter, sum ...
}
next retorna Some(elemento) até esgotar os dados, depois retorna None. Um loop for é na verdade apenas sintaxe açucarada para chamar next repetidamente.
4.2. iter, iter_mut e into_iter
Uma collection pode produzir três tipos diferentes de iterator:
fn main() {
let mut prices = vec![100, 200, 300];
for p in prices.iter() { // p: &i32
print!("{p} ");
}
println!();
for p in prices.iter_mut() { // p: &mut i32
*p += 10;
}
for p in prices.into_iter() { // p: i32, vetor foi movido
print!("{p} ");
}
println!();
// println!("{:?}", prices); // erro: valor emprestado após move
}
100 200 300
110 210 310
iter()oferece referências imutáveis. A collection ainda pode ser usada depois.iter_mut()oferece referências mutáveis para modificar elementos no local.into_iter()toma propriedade. Escreverfor p in pricestambém chamainto_iter()implicitamente.
5. Adaptadores e consumidores
Os métodos em iterator se dividem em dois grupos:
- Adaptadores (
map,filter,take,zip,enumerate…) transformam um iterator em outro. Eles são lazy: não executam nada até que haja um consumidor. - Consumidores (
collect,sum,count,fold,for_each,find…) realmente chamamnexte produzem um resultado.
Usaremos a struct Order como dados de exemplo:
#[derive(Debug)]
pub struct Order {
pub id: u32,
pub customer: String,
pub total: u32,
pub paid: bool,
}
pub fn unpaid_total(orders: &[Order]) -> u32 {
orders.iter()
.filter(|o| !o.paid)
.map(|o| o.total)
.sum()
}
pub fn big_customers(orders: &[Order], threshold: u32) -> Vec<&str> {
orders.iter()
.filter(|o| o.total >= threshold)
.map(|o| o.customer.as_str())
.collect()
}
fn main() {
let orders = vec![
Order { id: 1, customer: "An".into(), total: 150_000, paid: true },
Order { id: 2, customer: "Bình".into(), total: 80_000, paid: false },
Order { id: 3, customer: "Chi".into(), total: 320_000, paid: false },
];
println!("Não coletado: {}", unpaid_total(&orders));
println!("Grandes clientes: {:?}", big_customers(&orders, 100_000));
let summary = orders.iter()
.fold(String::new(), |acc, o| acc + &format!("#{} ", o.id));
println!("Pedidos: {summary}");
}
Não coletado: 400000
Grandes clientes: ["An", "Chi"]
Pedidos: #1 #2 #3
Os passos no exemplo:
filter(|o| !o.paid)recebe uma closure que retornabool. O parâmetrootem tipo&&Order, porquefilterpassa uma referência ao elemento, e o elemento deiter()já é&Order. Auto-deref permite que você escrevao.paidnormalmente.map(|o| o.total)transforma cada pedido em um valor monetário.sum()soma todos eles.collect()agrupa o resultado em uma collection. O tipo de destino é inferido do tipo de retornoVec<&str>da função.foldrecebe um valor inicial e uma closure(acc, elemento) -> acc_novo. Este é o consumidor mais geral.
5.1. Computação lazy
O trecho a seguir mostra que adaptadores só executam quando consumidos:
fn main() {
let ids = vec![1, 2, 3];
let doubled = ids.iter().map(|x| {
println!("map {x}");
x * 2
});
println!("Nada foi executado ainda");
let result: Vec<i32> = doubled.collect();
println!("{result:?}");
}
Nada foi executado ainda
map 1
map 2
map 3
[2, 4, 6]
Graças à computação lazy, a corrente filter → map → sum percorre os dados apenas uma vez e não cria collections intermediárias. O compilador geralmente otimiza esse pipeline para código de máquina equivalente a um loop escrito manualmente. Esta é a razão pela qual iterator é chamado de zero-cost abstraction.
6. Escrevendo seu próprio iterator
Você só precisa implementar next. Todos os outros adaptadores vêm de graça. O exemplo a seguir gera números de fatura no formato NF-0001:
pub struct InvoiceNumbers {
current: u32,
end: u32,
}
impl InvoiceNumbers {
pub fn new(start: u32, end: u32) -> Self {
Self { current: start, end }
}
}
impl Iterator for InvoiceNumbers {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.current > self.end {
return None;
}
let number = format!("NF-{:04}", self.current);
self.current += 1;
Some(number)
}
}
fn main() {
let customers = ["An", "Bình", "Chi"];
for (invoice, customer) in InvoiceNumbers::new(7, 100).zip(customers) {
println!("{invoice} -> {customer}");
}
}
NF-0007 -> An
NF-0008 -> Bình
NF-0009 -> Chi
Explicação:
type Item = Stringdeclara o tipo do elemento que o iterator retorna.nextatualiza o estado através de&mut selfe retornaNonequando excedeend.zipemparelha dois iterators em tuplas e para quando o iterator mais curto termina. Por isso apenas três linhas são impressas, mesmo que o intervalo vá até 100.
7. Escrevendo testes
Rust tem um framework de teste integrado. Você coloca o módulo de teste no mesmo arquivo que unpaid_total e InvoiceNumbers:
#[cfg(test)]
mod tests {
use super::*;
fn order(id: u32, total: u32, paid: bool) -> Order {
Order { id, customer: format!("CL{id}"), total, paid }
}
#[test]
fn unpaid_total_only_counts_unpaid_orders() {
let orders = vec![order(1, 100, true), order(2, 50, false), order(3, 70, false)];
assert_eq!(unpaid_total(&orders), 120);
}
#[test]
fn unpaid_total_of_empty_slice_is_zero() {
assert_eq!(unpaid_total(&[]), 0);
}
#[test]
fn invoice_numbers_stop_after_end() {
let numbers: Vec<String> = InvoiceNumbers::new(9, 11).collect();
assert_eq!(numbers, vec!["NF-0009", "NF-0010", "NF-0011"]);
}
}
cargo test
running 3 tests
test tests::invoice_numbers_stop_after_end ... ok
test tests::unpaid_total_of_empty_slice_is_zero ... ok
test tests::unpaid_total_only_counts_unpaid_orders ... ok
test result: ok. 3 passed; 0 failed; 0 ignored
O segundo teste verifica um caso extremo. sum() em um iterator vazio retorna 0, sem panic.
8. Erros comuns e quando não usar
Nota: Esquecer o consumidor é o erro mais comum. A linha
orders.iter().map(|o| println!("{}", o.id));não imprime nada. O compilador avisa “iterators are lazy and do nothing unless consumed”. Quando você precisa apenas de efeitos colaterais, use um loopforoufor_each.
Alguns outros erros que você encontrará:
collectnão sabe o tipo de destino. Escreverlet v = it.collect();resulta em erro “type annotations needed”. Você precisa ser explícito:let v: Vec<_> = ...ou use turbofishcollect::<Vec<_>>().- Usar
into_iter()e depois tentar usar a collection novamente. Se você precisa apenas ler, useiter(). - Closure segura o empréstimo muito tempo. Uma closure
FnMutcapturando&mut cartimpede que você leiacartaté a última chamada da closure. Restrinja o escopo da closure ou chame-a primeiro e depois leia. - Precisar de
?dentro demap. O operador?em uma closure retorna apenas da closure, não da função externa. Usecollect::<Result<Vec<_>, _>>(). Isso para no primeiro erro e retornaErr.
Quando não forçar tudo em uma corrente de iterator:
- Lógica com muitas ramificações, múltiplos pontos de
break/continue, ou atualizando várias variáveis ao mesmo tempo. Um loopforexplícito é mais legível do que umfoldde dez linhas. - Você precisa retornar closures de múltiplas ramificações, mas se preocupa com desempenho.
Box<dyn Fn>tem overhead de alocação e chamada dinâmica. Considere usarenumou genéricos em vez disso.
9. Conclusão
Closures em Rust capturam variáveis da forma mais leve possível: empréstimo, empréstimo mutável, ou move. A partir daí, a closure implementa automaticamente Fn, FnMut ou FnOnce. Ao escrever uma função que recebe closure, escolha o trait mais permissivo que a função precisa.
Iterator se baseia em apenas um método next. Adaptadores como map, filter, zip são lazy, enquanto consumidores como collect, sum, fold realmente executam o pipeline. Combinando essas duas ferramentas, você escreve código conciso, seguro em memória e tão rápido quanto loops manuais. Você também deve saber quando um simples loop for é a melhor escolha.