Closures em Rust
Uma closure (função anônima) é uma função pequena e sem nome que você pode guardar em uma variável e passar de um lado para o outro. O que a diferencia de uma função comum é que ela consegue “capturar” variáveis do escopo em que foi criada.
Sintaxe básica de uma closure
Seção intitulada “Sintaxe básica de uma closure”fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Saída:
6As barras verticais em |x: i32| fazem o papel dos parênteses que uma função comum usaria. Compare as duas lado a lado:
fn add_one_fn(x: i32) -> i32 { x + 1 } // uma função comumlet add_one_closure = |x: i32| x + 1; // a mesma coisa como closureMuitas vezes o Rust também consegue deduzir os tipos do parâmetro e do retorno, então é comum ver closures escritas de forma ainda mais curta:
let add_one = |x| x + 1;Uma closure sem parâmetros usa as barras vazias: let say_hi = || println!("oi");, e você a chama com say_hi().
Closures com várias linhas
Seção intitulada “Closures com várias linhas”Se houver mais de uma expressão, coloque o corpo entre chaves, como em uma função:
fn main() { let describe = |n: i32| { let kind = if n % 2 == 0 { "par" } else { "ímpar" }; format!("{n} é {kind}") };
println!("{}", describe(7));}Saída:
7 é ímparO format! funciona como o println!, mas, em vez de imprimir o texto, ele o devolve como uma String. Aqui, essa String é a última expressão dentro das chaves, então é ela que a closure retorna.
Capturando variáveis do escopo ao redor
Seção intitulada “Capturando variáveis do escopo ao redor”Este é o recurso que uma função comum não tem: uma closure pode usar variáveis do lugar onde foi definida, sem que você precise passá-las explicitamente:
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}Saída:
9040O discount não é um parâmetro da closure: ele só está ali no escopo da main, e a closure o alcança e usa diretamente. Uma fn comum não consegue fazer isso; ela precisaria receber discount como argumento.
Closures que alteram variáveis capturadas
Seção intitulada “Closures que alteram variáveis capturadas”Uma closure também pode alterar uma variável que ela captura, desde que essa variável seja mut. Existe só uma regra a mais: a própria closure também precisa ser guardada em uma variável mut. Chamá-la muda alguma coisa, e o Rust quer que isso fique visível no seu código.
fn main() { let mut count = 0;
let mut increment = || { count += 1; println!("a contagem agora é {count}"); };
increment(); increment(); increment();
println!("contagem final: {count}");}Saída:
a contagem agora é 1a contagem agora é 2a contagem agora é 3contagem final: 3Se você esquecer o mut em increment, o compilador diz exatamente o que está errado: calling increment requires mutable binding due to mutable borrow of count. Ou seja, chamar increment exige uma variável mutável, porque a closure pega count emprestado de forma mutável.
Tem um detalhe que vale a pena conhecer. Enquanto a closure ainda puder ser chamada, ela tem o direito de alterar count, então ninguém mais pode mexer em count. Se você colocar um println!("{count}"); entre duas chamadas de increment(), recebe error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable: não dá para pegar count emprestado para leitura porque ele já está emprestado para alteração. Depois da última chamada de increment(), a closure terminou o serviço, e count volta a ser todo seu. É por isso que a linha da contagem final funciona. Você vai conhecer a história completa por trás dessa regra na página de Borrowing (empréstimo).
Às vezes uma closure precisa ficar com o ownership (posse) completo do que captura, por exemplo quando ela é entregue a outra thread. Para isso, você escreve move antes das barras: move || { ... }. Você vai ver isso em ação na página de Concorrência.
Onde as closures aparecem na prática
Seção intitulada “Onde as closures aparecem na prática”Você não vai usar closures sozinhas com muita frequência. Onde elas são realmente úteis é para passar um comportamento personalizado para outra função, principalmente para os métodos de iteradores que você vai conhecer na página de Iteradores:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}Saída:
[2, 4, 6, 8, 10]Aqui, |n| n * 2 é uma closure passada direto para o .map(). Esse padrão (uma closure pequena comandando um método como .map() ou .filter()) é onde você vai usá-las de verdade no dia a dia.
Prática
Seção intitulada “Prática”1. Preços com imposto
Seção intitulada “1. Preços com imposto”Crie uma variável tax_percent com o valor 20. Depois, escreva uma closure with_tax que recebe um preço e devolve o preço mais o imposto, usando o tax_percent capturado. Imprima o resultado para os preços 50 e 120. Dica: o imposto é price * tax_percent / 100.
Ver solução
fn main() { let tax_percent = 20; let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 com imposto dá {}", with_tax(50)); println!("120 com imposto dá {}", with_tax(120));}Saída:
50 com imposto dá 60120 com imposto dá 144O tax_percent nunca é passado como argumento. A closure o captura do main, que é exatamente o que uma fn comum não conseguiria fazer.
2. Um gerador de IDs
Seção intitulada “2. Um gerador de IDs”Escreva uma closure next_id que devolve 1 na primeira vez que você a chama, 2 na segunda, 3 na terceira, e assim por diante. Chame-a três vezes e imprima cada ID. Dica: mantenha um contador mut fora da closure, some 1 a ele lá dentro e faça do contador a última expressão da closure, para que ele seja retornado.
Ver solução
fn main() { let mut last_id = 0;
let mut next_id = || { last_id += 1; last_id };
println!("o primeiro usuário recebe o id {}", next_id()); println!("o segundo usuário recebe o id {}", next_id()); println!("o terceiro usuário recebe o id {}", next_id());}Saída:
o primeiro usuário recebe o id 1o segundo usuário recebe o id 2o terceiro usuário recebe o id 3Tanto last_id quanto next_id precisam de mut: o primeiro porque o valor dele muda, e o segundo porque chamar a closure altera o que ela capturou.
- Uma closure é uma função anônima pequena:
|parâmetros| expressão. - Os tipos costumam ser deduzidos, então as closures geralmente são mais curtas do que uma
fnequivalente. - Ao contrário de uma função comum, uma closure pode capturar variáveis do escopo em que foi definida.
- Uma closure que altera uma variável capturada precisa ser guardada em uma variável
mut, e ela pega essa variável emprestada enquanto estiver sendo usada. - As closures são mais úteis quando passadas para outras funções, principalmente métodos de iteradores como o
.map().
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Que símbolos uma closure usa no lugar dos parênteses de uma função para os parâmetros?
2. O que uma closure consegue fazer que uma fn comum não consegue?
3. Segundo esta página, onde as closures são mais usadas na prática?
4. Uma closure faz count += 1 em uma variável capturada. Do que mais você precisa?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Closures — The Rust Book (em inglês): o capítulo oficial sobre closures, inferência de tipos e as traits
Fn. - Capturing References or Moving Ownership — The Rust Book (em inglês): as três formas como uma closure pode capturar uma variável, e quando usar
move. - Closures — Rust by Example (em inglês): exemplos curtos e executáveis comparando closures com funções.
- Capturing — Rust by Example (em inglês): mais exemplos de closures que leem, alteram e tomam posse de variáveis.
- Closure expressions — The Rust Reference (em inglês): as regras exatas da sintaxe e da captura em closures.