Closures
Esta aula aborda closures em Rust, incluindo sintaxe, captura do ambiente, as traits Fn, FnMut e FnOnce, e a palavra-chave move. O conteúdo é aprofundado com exemplos práticos e exercícios.
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 e poderosas, permitindo escrever código conciso e expressivo. Nesta aula, exploraremos a sintaxe, como closures capturam o ambiente, as traits que controlam como a captura ocorre e o uso da palavra-chave move.
Entender closures é fundamental para programação funcional em Rust, uso de iteradores, callbacks e concorrência. Vamos mergulhar nos detalhes.
Sintaxe
Closures em Rust têm uma sintaxe leve: parâmetros opcionais entre pipes | seguidos por um corpo. O corpo pode ser uma expressão única (sem chaves) ou um bloco com chaves. O tipo dos parâmetros e do retorno pode ser inferido ou explicitamente anotado.
Exemplo básico:
let add_one = |x: i32| -> i32 { x + 1 };
let result = add_one(5); // 6Se o corpo for uma única expressão, as chaves podem ser omitidas:
let add_one = |x| x + 1;Parâmetros múltiplos:
let add = |a, b| a + b;
println!("{}", add(2, 3)); // 5É possível anotar tipos explicitamente, mas geralmente o compilador infere.
Capturando o ambiente
Closures podem capturar variáveis do escopo externo por referência imutável (&T), referência mutável (&mut T) ou por valor (T). O compilador escolhe a forma menos restritiva possível, baseada no uso da variável dentro do closure.
Exemplo de captura por referência imutável:
let s = String::from("hello");
let print_s = || println!("{}", s); // captura &s
print_s();
println!("{}", s); // ainda válidoExemplo de captura por referência mutável:
let mut v = vec![1, 2, 3];
let mut push_to_v = |x| v.push(x); // captura &mut v
push_to_v(4);
// println!("{:?}", v); // erro: borrow mutável ativoExemplo de captura por valor (move):
let s = String::from("hello");
let consume_s = || drop(s); // captura s por valor
consume_s();
// println!("{}", s); // erro: s foi movidoFn, FnMut, FnOnce
Rust categoriza closures por três traits: Fn, FnMut e FnOnce. Elas determinam como o closure pode ser chamado e quantas vezes.
- FnOnce: Pode ser chamado uma vez. Captura variáveis por valor (move). O closure consome as variáveis.
- FnMut: Pode ser chamado múltiplas vezes e pode modificar o ambiente (captura por referência mutável).
- Fn: Pode ser chamado múltiplas vezes sem modificar o ambiente (captura por referência imutável).
O compilador implementa automaticamente a trait menos restritiva possível. Por exemplo, um closure que só lê variáveis implementa Fn (e também FnMut e FnOnce).
Exemplo com funções que aceitam diferentes traits:
fn call_once(f: impl FnOnce()) {
f();
}
fn call_mut(mut f: impl FnMut()) {
f();
f();
}
fn call_fn(f: impl Fn()) {
f();
f();
}
let s = String::from("hello");
let c = || println!("{}", s);
call_fn(&c);
call_mut(&mut c);
call_once(c); // moveNote que Fn é subtrait de FnMut, que é subtrait de FnOnce. Portanto, um closure que implementa Fn pode ser usado onde se espera FnMut ou FnOnce.
move
A palavra-chave move força o closure a capturar variáveis por valor, transferindo a propriedade para o closure. Isso é útil quando o closure precisa sobreviver ao escopo da variável original, por exemplo, em threads ou retornos de função.
Exemplo:
let s = String::from("hello");
let closure = move || {
println!("{}", s);
};
// println!("{}", s); // erro: s foi movido
closure();Sem move, o closure capturaria s por referência, e se o closure for enviado para uma thread, a referência pode se tornar inválida. Com move, garantimos que o closure possui os dados.
Outro uso comum é em iteradores com map ou filter quando queremos capturar variáveis por valor:
let nums = vec![1, 2, 3];
let multiplier = 2;
let result: Vec<_> = nums.into_iter().map(move |x| x * multiplier).collect();
println!("{:?}", result); // [2, 4, 6]Boas práticas
- Prefira capturar por referência quando possível para evitar clonagem desnecessária.
- Use
movequando o closure precisar ser enviado para outra thread ou armazenado em uma estrutura que sobreviva ao escopo. - Lembre-se de que closures que implementam
Fnsão as mais flexíveis; tente projetar suas APIs para aceitarFnquando possível. - Para closures que modificam o ambiente, use
FnMute garanta que a variável capturada seja mutável.
Referências
- The Rust Programming Language: Closures
- Rust Reference: Closure expressions
- std::ops::Fn
- std::ops::FnMut
- std::ops::FnOnce
- Rust by Example: Closures
Exercícios
Crie um closure que receba dois inteiros e retorne o maior deles. Atribua a uma variável e chame-o com valores 10 e 20.
✓ Resposta:let max = |a: i32, b: i32| if a > b { a } else { b }; println!("{}", max(10, 20)); // 20Escreva um closure que capture uma variável
Stringpor referência imutável e a imprima. Depois, chame o closure e também use a variável original.✓ Resposta:let s = String::from("hello"); let print_s = || println!("{}", s); print_s(); println!("{}", s); // ainda válidoCrie um closure que modifique um vetor (por exemplo, adicione um elemento) usando
FnMut. Atribua a uma variável mutável e chame-o duas vezes.✓ Resposta:let mut v = vec![1, 2, 3]; let mut push = |x| v.push(x); push(4); push(5); println!("{:?}", v); // [1, 2, 3, 4, 5]Escreva um closure que capture uma
Stringpor valor usandomovee a descarte comdrop. Depois, tente usar a variável original (não compile).✓ Resposta:let s = String::from("hello"); let drop_s = move || drop(s); drop_s(); // println!("{}", s); // erro: s foi movidoCrie uma função
apply_twiceque aceite um closureFn(i32) -> i32e um inteiro, e aplique o closure duas vezes ao inteiro. Use-a com um closure que dobra o valor.✓ Resposta:fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x)) } let double = |x| x * 2; println!("{}", apply_twice(double, 5)); // 20