Closures em Rust: captura por referência e por valor
1. Introdução às Closures em Rust
Closures em Rust são funções anônimas que podem capturar variáveis do escopo onde são definidas. Diferentemente de funções tradicionais, closures têm acesso ao ambiente ao seu redor, permitindo comportamentos dinâmicos e flexíveis.
A sintaxe básica de uma closure é simples: |parâmetros| expressão. Por exemplo:
let soma = |a: i32, b: i32| a + b;
println!("{}", soma(3, 4)); // 7
Rust infere os tipos dos parâmetros e do retorno na maioria dos casos, mas você pode anotá-los explicitamente:
let soma_anotada = |a: i32, b: i32| -> i32 { a + b };
A principal diferença entre closures e funções tradicionais é a capacidade de capturar variáveis do ambiente:
let fator = 2;
let multiplicar = |x: i32| x * fator; // captura 'fator' do ambiente
println!("{}", multiplicar(5)); // 10
2. Mecanismos de Captura: Referência Imutável, Referência Mutável e Valor
Rust oferece três modos de captura, determinados automaticamente pelo compilador baseado no uso da variável dentro da closure:
Captura por referência imutável (&T)
Quando a closure apenas lê a variável, Rust a captura como referência imutável:
let mensagem = String::from("Olá");
let imprimir = || println!("{}", mensagem); // captura &String
imprimir();
println!("Ainda posso usar: {}", mensagem); // funciona!
Captura por referência mutável (&mut T)
Quando a closure modifica a variável, Rust a captura como referência mutável:
let mut contador = 0;
let mut incrementar = || {
contador += 1; // captura &mut i32
println!("Contador: {}", contador);
};
incrementar(); // Contador: 1
incrementar(); // Contador: 2
// println!("{}", contador); // erro! emprestado mutavelmente
Captura por valor (T)
Quando a closure precisa mover a variável para seu interior (ex: para enviar a outra thread):
let nome = String::from("Alice");
let saudacao = || {
let _ = nome; // captura por valor, move ownership
println!("Tchau!");
};
saudacao();
// println!("{}", nome); // erro! nome foi movido
3. O Trait Fn, FnMut e FnOnce: Como o Compilador Escolhe
Rust classifica closures em três traits baseados no modo de captura:
Fn — Captura por referência imutável
Closures que não modificam o ambiente e podem ser chamadas múltiplas vezes sem efeitos colaterais:
fn executar_fn<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x) + f(x) // pode chamar várias vezes
}
let dobro = |x| x * 2;
println!("{}", executar_fn(dobro, 5)); // 20
FnMut — Captura por referência mutável
Closures que modificam o ambiente e podem ser chamadas múltiplas vezes:
fn executar_fnmut<F: FnMut(i32) -> i32>(mut f: F, x: i32) -> i32 {
f(x) + f(x) // precisa de mut
}
let mut acumulador = 10;
let mut soma = |x| {
acumulador += x;
acumulador
};
println!("{}", executar_fnmut(soma, 5)); // 20 (10+5 + 15+5)
FnOnce — Captura por valor
Closures que consomem o ambiente e podem ser chamadas apenas uma vez:
fn executar_fnonce<F: FnOnce(String) -> String>(f: F, s: String) -> String {
f(s) // só pode chamar uma vez
}
let prefixo = String::from("Sr. ");
let saudacao = |nome| format!("{}{}", prefixo, nome);
println!("{}", executar_fnonce(saudacao, "João".to_string())); // Sr. João
O compilador infere automaticamente o trait menos restritivo possível. Uma closure Fn também implementa FnMut e FnOnce, uma FnMut também implementa FnOnce, mas não Fn.
4. Captura por Referência: Implicações e Boas Práticas
Capturar por referência é eficiente e não transfere ownership, mas requer cuidado com lifetimes e regras de borrowing.
Exemplo com iteradores e filter:
let numeros = vec![1, 2, 3, 4, 5, 6];
let limite = 3;
let maiores: Vec<&i32> = numeros.iter()
.filter(|&&x| x > limite) // captura &limite por referência imutável
.collect();
println!("{:?}", maiores); // [4, 5, 6]
Boas práticas:
- Prefira captura por referência quando possível para evitar cópias desnecessárias
- Lembre-se que múltiplas closures podem compartilhar referências imutáveis simultaneamente
- Apenas uma closure pode ter referência mutável por vez
5. Captura por Valor: Movendo Ownership para a Closure
Use a palavra-chave move para forçar captura por valor, essencial quando a closure precisa sobreviver ao escopo original:
use std::thread;
let dados = vec![1, 2, 3, 4, 5];
thread::spawn(move || {
println!("Dados na thread: {:?}", dados);
// dados foi movido para a closure
}).join().unwrap();
// println!("{:?}", dados); // erro! dados foi movido
Cenários comuns para move:
- thread::spawn — a closure precisa ter ownership dos dados
- async tasks — dados precisam sobreviver a pontos de suspensão
- Retorno de closures de funções — a closure precisa ser independente
6. Closures como Argumentos de Função: Genéricos e Traits
Funções podem aceitar closures usando genéricos com impl Fn ou Box<dyn Fn>:
fn aplicar_tres_vezes<F>(mut f: F, x: i32) -> i32
where
F: FnMut(i32) -> i32,
{
f(f(f(x)))
}
let mut multiplicador = 2;
let mut multiplicar = |x| x * multiplicador;
println!("{}", aplicar_tres_vezes(multiplicar, 1)); // 8 (1*2*2*2)
impl Trait vs dyn Trait:
- impl Fn — monomorfização, melhor performance, tipos concretos
- Box<dyn Fn> — dispatch dinâmico, útil para coleções heterogêneas
fn coletar_closures() -> Vec<Box<dyn Fn(i32) -> i32>> {
vec![
Box::new(|x| x + 1),
Box::new(|x| x * 2),
Box::new(|x| x - 3),
]
}
let closures = coletar_closures();
for f in closures {
println!("{}", f(10));
}
7. Closures Retornadas de Funções e Lifetimes
Retornar closures que capturam referências requer anotações de lifetime:
fn criar_saudacao<'a>(nome: &'a str) -> impl Fn() -> String + 'a {
move || format!("Olá, {}!", nome)
}
let nome = String::from("Maria");
let saudacao = criar_saudacao(&nome);
println!("{}", saudacao()); // Olá, Maria!
Sem a anotação de lifetime, o compilador não pode garantir que a referência ainda será válida quando a closure for chamada. O uso de move força a captura por valor, resolvendo problemas de dangling references.
8. Casos Avançados e Erros Comuns
Captura mista de múltiplas variáveis
let mut a = 5;
let b = 10;
let closure = || {
a += 1; // captura &mut a
println!("{}", b); // captura &b
};
closure();
// println!("{}", a); // erro! emprestado mutavelmente
Erro de borrowing conflitante
let mut lista = vec![1, 2, 3];
let ref1 = &lista;
let closure = || {
lista.push(4); // erro! já emprestado imutavelmente
};
println!("{:?}", ref1);
Closures aninhadas
let externo = String::from("externa");
let interna = {
let mut contador = 0;
move || {
contador += 1;
format!("{} {}", externo, contador)
}
};
println!("{}", interna()); // externa 1
println!("{}", interna()); // externa 2
Dicas de depuração: Quando encontrar erros com closures, leia atentamente as mensagens do compilador. Elas geralmente indicam qual variável foi movida ou emprestada, e em qual closure ocorreu o conflito. Use --explain para obter explicações detalhadas.
Referências
- Closures - Rust Book — Capítulo oficial sobre closures no Rust Book, com exemplos detalhados de captura e traits
- Rust Reference: Closure expressions — Documentação oficial da sintaxe e semântica de closures em Rust
- Fn, FnMut, FnOnce - Rust API Docs — Documentação completa dos traits de closure na biblioteca padrão
- Closures: Capturing the Environment - Rust by Example — Tutorial prático sobre os três modos de captura com exemplos interativos
- Rust Closures: A Comprehensive Guide — Artigo técnico aprofundado sobre closures, incluindo casos avançados e padrões comuns