Closures
Nesta aula, você aprenderá sobre closures em Rust: sintaxe, captura de ambiente, as traits Fn, FnMut e FnOnce, e a palavra-chave move. Com exemplos práticos e exercícios, dominará como criar e usar closures de forma eficiente.
Closures em Rust são funções anônimas que podem capturar variáveis do escopo onde são definidas. Elas são flexíveis, podem ser armazenadas em variáveis, passadas como argumentos e retornadas de funções. Nesta aula, exploraremos a sintaxe das closures, como elas capturam o ambiente, as três traits que as regem (Fn, FnMut, FnOnce) e o uso da palavra-chave move para transferir ownership.
Entender closures é fundamental para programação funcional em Rust, especialmente ao trabalhar com iteradores, callbacks e concorrência. Vamos mergulhar nos detalhes.
Sintaxe
Closures têm uma sintaxe concisa. A forma mais simples é |parâmetros| expressão. Se o corpo tiver múltiplas linhas, use chaves. Exemplo:
let soma = |a: i32, b: i32| -> i32 { a + b };
println!("{}", soma(2, 3)); // 5Os tipos dos parâmetros e do retorno podem ser inferidos, mas podem ser explicitados. Se o corpo for uma única expressão, as chaves são opcionais:
let multiplica = |x, y| x * y;
println!("{}", multiplica(4, 5)); // 20Capturando o ambiente
Closures podem capturar variáveis do escopo externo por referência imutável, referência mutável ou por valor (ownership). O compilador decide automaticamente o tipo de captura com base no uso da variável dentro da closure.
Exemplo de captura por referência imutável:
let nome = String::from("Alice");
let saudacao = || println!("Olá, {}", nome);
saudacao(); // Olá, Alice
println!("{}", nome); // ainda válidoSe a closure modificar a variável, ela capturará por referência mutável:
let mut contador = 0;
let mut incrementa = || contador += 1;
incrementa();
println!("{}", contador); // 1Se a variável for movida para dentro da closure (por exemplo, usando move), ela é capturada por valor.
Fn, FnMut, FnOnce
Toda closure implementa pelo menos uma das traits: Fn, FnMut ou FnOnce. Elas determinam como a closure pode ser chamada e o que ela pode fazer com as variáveis capturadas.
- FnOnce: Pode ser chamada uma vez. Consome as variáveis capturadas. Toda closure implementa FnOnce.
- FnMut: Pode ser chamada múltiplas vezes e pode modificar o ambiente capturado (referência mutável).
- Fn: Pode ser chamada múltiplas vezes sem modificar o ambiente (referência imutável). É a mais restritiva.
O compilador escolhe a trait mais específica que a closure pode implementar. Por exemplo, uma closure que só lê variáveis implementa Fn (e por consequência FnMut e FnOnce).
Exemplo:
fn executa_fn(f: impl Fn()) { f(); }
fn executa_fn_mut(mut f: impl FnMut()) { f(); }
fn executa_fn_once(f: impl FnOnce()) { f(); }
let x = 10;
let closure = || println!("{}", x);
executa_fn(closure);
executa_fn_mut(closure);
executa_fn_once(closure);move
A palavra-chave move força a closure a tomar ownership das variáveis capturadas, em vez de emprestar referências. Isso é útil quando a closure precisa ser enviada para outra thread ou armazenada em uma estrutura que viva mais que o escopo original.
let s = String::from("Olá");
let closure = move || {
println!("{}", s);
};
// println!("{}", s); // erro: s foi movido
closure();Sem move, a closure capturaria s por referência, mas se a closure for chamada após s sair de escopo, causaria erro. move garante que a closure possua os dados.
Boas práticas
Prefira usar move apenas quando necessário, para evitar cópias desnecessárias. Em closures que serão passadas para funções que exigem FnOnce (como thread::spawn), move é obrigatório. Além disso, closures são frequentemente usadas com iteradores, como map, filter, for_each.
Referências
- The Rust Book: Closures
- Rust by Example: Closures
- Rust std::ops::Fn
- Rust std::ops::FnMut
- Rust std::ops::FnOnce
- Rust Reference: Closure types
Exercícios
Crie uma closure que receba dois inteiros e retorne a soma deles. Atribua a uma variável e chame-a com os valores 5 e 7. Imprima o resultado.
✓ Resposta:let soma = |a: i32, b: i32| -> i32 { a + b }; println!("{}", soma(5, 7)); // 12Escreva uma closure que capture uma variável externa
nome: Stringe a imprima. Usemovepara garantir que a closure possua a string. Depois de definir a closure, tente usarnomefora dela e veja o erro.✓ Resposta:let nome = String::from("Maria"); let closure = move || println!("{}", nome); closure(); // println!("{}", nome); // erro: nome foi movidoCrie uma função
aplica_tres_vezesque recebe uma closurefe um valorx, e aplicaftrês vezes sobrex. Use a traitFnpara o parâmetro. Teste com uma closure que dobra o valor.✓ Resposta:fn aplica_tres_vezes<F>(f: F, x: i32) -> i32 where F: Fn(i32) -> i32, { f(f(f(x))) } fn main() { let dobra = |n| n * 2; println!("{}", aplica_tres_vezes(dobra, 1)); // 8 }Crie uma closure que modifica uma variável externa
total(mutável) adicionando um valor. UseFnMute chame a closure duas vezes. Imprima o total após cada chamada.✓ Resposta:let mut total = 0; let mut adiciona = |valor: i32| total += valor; adiciona(10); println!("{}", total); // 10 adiciona(5); println!("{}", total); // 15Escreva uma função que recebe um vetor de números e uma closure
fque implementaFnOnce(que consome o vetor) e aplicafao vetor. Teste com uma closure que imprime a soma dos elementos.✓ Resposta:fn processa_vetor<F>(v: Vec<i32>, f: F) where F: FnOnce(Vec<i32>), { f(v); } fn main() { let numeros = vec![1, 2, 3, 4, 5]; processa_vetor(numeros, |v| { let soma: i32 = v.iter().sum(); println!("{}", soma); // 15 }); }