Os closures (fechamentos) de Rust são funções anônimas que você pode salvar em uma variável ou transmitir como argumentos para outras funções. Você pode criar closure em um único local e depois chamá-lo para avaliá-lo em um contexto diferente. Diferentemente das funções, os closures podem capturar valores do escopo em que são chamados. Vamos demonstrar como esses recursos de closure permitem a reutilização de código e a personalização do comportamento.
Vamos trabalhar em um exemplo de situação em que é útil armazenar um closure para ser executado posteriormente. Ao longo do caminho, falaremos sobre a sintaxe de closures, inferência de tipos e traits.
Considere esta situação hipotética: trabalhamos em uma startup que está criando um aplicativo para gerar planos personalizados de treinos (exercícios). O back-end é escrito em Rust, e o algoritmo que gera o plano de treinos leva em consideração muitos fatores, como idade do usuário do aplicativo, índice de massa corporal, preferências de treinos, treinos recentes e um número de intensidade que eles especificam. O algoritmo real usado não é importante neste exemplo; o importante é que esse cálculo leve alguns segundos. Queremos chamar esse algoritmo apenas quando precisarmos e apenas uma vez, para não fazer o usuário esperar mais do que o necessário.
Simularemos a chamada desse algoritmo hipotético com a função
simulated_expensive_calculation
mostrado na Listagem 13-1, que imprimirá
calculating slowly...
(calculando lentamente...), aguardará dois segundos e retornará qualquer
número que tenhamos passado:
Nome do arquivo: src/main.rs
use std::thread;
use std::time::Duration;
fn simulated_expensive_calculation(intensity: u32) -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
intensity
}
Listagem 13-1: Uma função para substituir um cálculo hipotético que leva cerca de 2 segundos para ser executado
A seguir, a função main
, que contém as partes do aplicativo de treino importantes
para este exemplo. Esta função representa o código que o aplicativo chamará
quando um usuário solicitar um plano de treino. Como a interação com o front-end do
aplicativo não é relevante para o uso de closure, codificaremos os valores que
representam as entradas do nosso programa e imprimiremos as saídas.
As entradas necessárias são estas:
- Um número de intensidade do usuário, especificado quando ele solicita um treino para indicar se deseja um treino de baixa intensidade ou um treino de alta intensidade
- Um número aleatório que irá gerar alguma variedade nos planos de treino
A saída será o plano de treino recomendado. A Listagem 13-2 mostra a função main
que usaremos:
Nome do arquivo: src/main.rs
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(
simulated_user_specified_value,
simulated_random_number
);
}
# fn generate_workout(intensity: u32, random_number: u32) {}
Listagem 13-2: Uma função main
com valores codificados
para simular a entrada do usuário e a geração aleatória de números
Codificamos a variável simulated_user_specified_value
como 10 e a variável
simulated_random_number
como 7 por uma questão de simplicidade; em um programa
real, obteríamos o número de intensidade da interface do aplicativo e usaríamos
a crate rand
para gerar um número aleatório, como fizemos no exemplo do jogo
de adivinhação no capítulo 2. A função main
chama uma função generate_workout
com os valores de entrada simulados.
Agora que temos o contexto, vamos ao algoritmo. A função
generate_workout
na Listagem 13-3 contém a lógica de negócios do
aplicativo com o qual estamos mais preocupados neste exemplo. O restante
das alterações de código neste exemplo será feito para esta função.
Nome do arquivo: src/main.rs
# use std::thread;
# use std::time::Duration;
#
# fn simulated_expensive_calculation(num: u32) -> u32 {
# println!("calculating slowly...");
# thread::sleep(Duration::from_secs(2));
# num
# }
#
fn generate_workout(intensity: u32, random_number: u32) {
if intensity < 25 {
println!(
"Today, do {} pushups!",
simulated_expensive_calculation(intensity)
);
println!(
"Next, do {} situps!",
simulated_expensive_calculation(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
simulated_expensive_calculation(intensity)
);
}
}
}
Listagem 13-3: A lógica de negócios que imprime os planos
de treino com base nas entradas e chama a função simulated_expensive_calculation
O código na Listagem 13-3 possui várias chamadas para a função de cálculo lento.
O primeiro bloco if
chama simulated_expensive_calculation
duas vezes, o if
dentro do else
externo não é chamado, e o código dentro do segundo caso else
o chama uma vez.
O comportamento desejado da função generate_workout
é primeiro verificar se
o usuário deseja um treino de baixa intensidade (indicado por um número menor
que 25) ou um treino de alta intensidade (um número igual ou superior a 25).
Os planos de treino de baixa intensidade recomendam várias flexões e abdominais com base no algoritmo complexo que estamos simulando.
Se o usuário deseja um treino de alta intensidade, há uma lógica adicional: se o valor do número aleatório gerado pelo aplicativo for 3, o aplicativo recomendará uma pausa e hidratação. Caso contrário, o usuário terá vários minutos de execução com base no algoritmo complexo.
Esse código funciona da maneira que a empresa (negócio) deseja agora, mas digamos que a
equipe de ciência de dados decida que precisamos fazer algumas alterações no futuro
na forma como chamamos a função simulated_expensive_calculation
. Para simplificar
a atualização quando essas alterações ocorrerem, queremos refatorar esse código para
que ele chame a função simulated_expensive_calculation
apenas uma vez. Também queremos
reduzir o local em que estamos chamando a função desnecessariamente duas vezes, sem adicionar
outras chamadas a essa função no processo. Ou seja, não queremos chamá-lo se o resultado
não for necessário e ainda queremos chamá-lo apenas uma vez.
Poderíamos reestruturar o programa de treinos de várias maneiras. Primeiro,
tentaremos extrair a chamada duplicada para a função simulated_expensive_calculation
em uma variável, conforme mostrado na Listagem 13-4:
Nome do arquivo: src/main.rs
# use std::thread;
# use std::time::Duration;
#
# fn simulated_expensive_calculation(num: u32) -> u32 {
# println!("calculating slowly...");
# thread::sleep(Duration::from_secs(2));
# num
# }
#
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_result =
simulated_expensive_calculation(intensity);
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_result
);
println!(
"Next, do {} situps!",
expensive_result
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result
);
}
}
}
Listagem 13-4: Extraindo as chamadas para
simulated_expensive_calculation
em um único local e armazenando o resultado na
variável expensive_result
Esta mudança unifica todas as chamadas para simulated_expensive_calculation
e
resolve o problema do primeiro bloco if
chamando desnecessariamente a função duas
vezes. Infelizmente, agora estamos chamando essa função e aguardando o resultado em
todos os casos, o que inclui o bloco interno if
que não usa o valor do resultado.
Queremos definir o código em um local do nosso programa, mas apenas executar o código onde realmente precisamos do resultado. Este é um caso de uso para closures!
Em vez de sempre chamar a função simulated_expensive_calculation
antes dos blocos if
,
podemos definir um closure e armazenar o closure em uma variável em vez de armazenar
o resultado da chamada de função, como mostra a Listagem 13-5. Na verdade, podemos mover
todo o corpo de simulated_expensive_calculation
dentro do closure que estamos
apresentando aqui:
Nome do arquivo: src/main.rs
# use std::thread;
# use std::time::Duration;
#
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
# expensive_closure(5);
Listagem 13-5: Definindo um closure e armazenando-o
na variável expensive_closure
A definição de closure vem após o =
para atribuí-lo à variável expensive_closure
.
Para definir um closure, começamos com um par de tubos (pipes) verticais (|
), dentro
dos quais especificamos os parâmetros para closure; essa sintaxe foi escolhida devido à
sua semelhança com as definições de closure em Smalltalk e Ruby. Esse closure possui um
parâmetro chamado num
: se tivéssemos mais de um parâmetro, os separaríamos com vírgulas,
como |param1, param2|
.
Após os parâmetros, colocamos colchetes que seguram o corpo do closure; eles são
opcionais se o corpo do closure for uma expressão única. O final do closure, depois
dos colchetes, precisa de um ponto e vírgula para concluir a declaração let
.
O valor retornado da última linha no corpo do closure (num
) será o valor retornado
do closure quando for chamado, porque essa linha não termina em ponto e vírgula;
assim como nos corpos funcionais.
Observe que esta declaração let
significa que expensive_closure
contém a definição
de uma função anônima, não o valor resultante de chamar a função anônima. Lembre-se de
que estamos usando um closure porque queremos definir o código para chamar em um ponto,
armazenar esse código e chamá-lo em um momento posterior; o código que queremos chamar
agora está armazenado em expensive_closure
.
Com o closure definido, podemos alterar o código nos blocos if
para chamar o closure
para executar o código e obter o valor resultante. Chamamos o closure como fazemos com
uma função: especificamos o nome da variável que mantém a definição de closure e a
seguimos com parênteses contendo os valores do argumento que queremos usar,
como mostra a Listagem 13-6:
Nome do arquivo: src/main.rs
# use std::thread;
# use std::time::Duration;
#
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_closure(intensity)
);
println!(
"Next, do {} situps!",
expensive_closure(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
Listagem 13-6: Chamando o expensive_closure
que definimos
Agora, o cálculo caro (expensive; lento) é chamado em apenas um lugar, e estamos apenas executando esse código onde precisamos dos resultados.
No entanto, reintroduzimos um dos problemas da Lista 13-3: ainda estamos chamando
closure duas vezes no primeiro bloco if
, que chamará o código caro duas vezes
e fará com que o usuário espere o dobro do tempo necessário. Podemos resolver esse
problema criando uma variável local para esse bloco if
para conter o resultado
de chamar o closure, mas os closures nos fornecem outra solução. Falaremos sobre
essa solução daqui a pouco. Mas primeiro vamos falar sobre por que não há anotações
de tipo na definição de closure e as traits envolvidas nos closures.
Os closures não exigem que você anote os tipos dos parâmetros ou o valor de retorno,
como as funções fn
. As anotações de tipo são necessárias nas funções porque fazem
parte de uma interface explícita exposta aos seus usuários. Definir rigidamente essa
interface é importante para garantir que todos concordem com os tipos de valores que
uma função usa e retorna. Mas os closures não são usados em uma interface exposta como
esta: eles são armazenados em variáveis e usados sem nomeá-los e expô-los aos usuários
de nossa biblioteca.
Os closures são geralmente curtos e relevantes apenas dentro de um contexto estreito (específico) e não em qualquer cenário arbitrário. Nesses contextos limitados, o compilador é capaz de inferir com segurança os tipos dos parâmetros e o tipo de retorno, semelhante à forma como é capaz de inferir os tipos da maioria das variáveis.
Fazer com que os programadores anotem os tipos nessas pequenas funções anônimas seria irritante e amplamente redundante com as informações que o compilador já tem disponível.
Assim como nas variáveis, podemos adicionar anotações de tipo se quisermos aumentar explicitação e clareza ao custo de ser mais detalhado do que o estritamente necessário. Anotar os tipos para o closure que definimos na Lista 13-5 seria semelhante à definição mostrada na Lista 13-7:
Nome do arquivo: src/main.rs
# use std::thread;
# use std::time::Duration;
#
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
Listagem 13-7: Incluindo anotações de tipo opcional do parâmetro e retornando tipos de valor no closure
Com as anotações de tipo adicionadas, a sintaxe dos closures se parece mais com a sintaxe das funções. A seguir, é apresentada uma comparação vertical da sintaxe para a definição de uma função que adiciona 1 ao seu parâmetro e um closure que tem o mesmo comportamento. Adicionamos alguns espaços para alinhar as partes relevantes. Isso ilustra como a sintaxe de closure é semelhante à sintaxe de função, exceto pelo uso de pipes e a quantidade de sintaxe opcional:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
A primeira linha mostra uma definição de função e a segunda linha mostra uma definição de closure totalmente anotada. A terceira linha remove as anotações de tipo da definição na closure e a quarta linha remove os colchetes, que são opcionais porque o corpo do closure possui apenas uma expressão. Todas essas são definições válidas que produzirão o mesmo comportamento quando forem chamadas.
As definições de closure terão um tipo concreto inferido para cada um de seus parâmetros
e para seu valor de retorno. Por exemplo, a Listagem 13-8 mostra a definição de um closure
curto que apenas retorna o valor que recebe como parâmetro. Esse closure não é muito útil,
exceto para os fins deste exemplo. Observe que não adicionamos anotações de tipo à definição:
se tentarmos chamar o closure duas vezes, usando uma String
como argumento na primeira
vez e um u32
na segunda vez, obteremos um erro .
Nome do arquivo: src/main.rs
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
Listing 13-8: Attempting to call a closure whose types are inferred with two different types
The compiler gives us this error:
error[E0308]: mismatched types
--> src/main.rs
|
| let n = example_closure(5);
| ^ expected struct `std::string::String`, found
integral variable
|
= note: expected type `std::string::String`
found type `{integer}`
A primeira vez que chamamos example_closure
com o valor String
, o compilador
deduz o tipo de x
e o tipo de retorno do closure para String
. Esses tipos
são bloqueados no closure em example_closure
, e obtemos um erro de tipo se
tentarmos usar um tipo diferente com o mesmo closure.
Vamos voltar ao nosso aplicativo de geração de treinos. Na Listagem 13-6, nosso código ainda estava chamando o closure de cálculo caro mais vezes do que o necessário. Uma opção para resolver esse problema é salvar o resultado do closure caro em uma variável para reutilização e usar a variável em cada local em que precisamos do resultado, em vez de chamar o closure novamente. No entanto, esse método pode resultar em muitos códigos repetidos.
Felizmente, outra solução está disponível para nós. Podemos criar uma estrutura que reterá o closure e o valor resultante de chamar o closure. A estrutura executará o closure apenas se precisarmos do valor resultante e armazenará em cache o valor resultante, para que o restante do nosso código não seja responsável por salvar e reutilizar o resultado. Você pode conhecer esse padrão como memoization (memorização) ou lazy evaluation (avaliação lenta).
Para criar uma estrutura que mantenha um closure, precisamos especificar o tipo do closure, porque uma definição de estrutura precisa conhecer os tipos de cada um de seus campos. Cada instância do closure tem seu próprio tipo anônimo: ou seja, mesmo que dois closures tenham a mesma assinatura, seus tipos ainda são considerados diferentes. Para definir estruturas, enumerações ou parâmetros de função que usam closures, usamos limites (bounds) genéricos e de traits, conforme discutimos no Capítulo 10.
As traits Fn
são fornecidas pela biblioteca padrão. Todos os closures implementam
pelo menos uma das traits: Fn
, FnMut
ou FnOnce
. Discutiremos a diferença entre
essas traits na seção "Capturando o Ambiente com Closures"; Neste exemplo, podemos
usar a trait Fn
.
Nós adicionamos tipos ao trait Fn
vinculado, para representar os tipos dos
parâmetros e retornamos valores que closures devem ter para corresponder
a esse trait vinculado. Nesse caso, nosso closure possui um parâmetro do tipo
u32
e retorna um u32
, portanto a trait que especificamos é Fn(u32) -> u32
.
A Listagem 13-9 mostra a definição da estrutura do Cacher
que contém um closure
e um valor de resultado opcional:
Nome do arquivo: src/main.rs
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
Listagem 13-9: Definindo uma estrutura Cacher
que mantém
um closure em calculation
e um resultado opcional em value
A estrutura Cacher
possui um campo calculation
do tipo genérico T
. Os limites
da trait em T
especificam que é um closure usando trait Fn
. Qualquer
closure que queremos armazenar no campo calculation
deve ter um parâmetro u32
(especificado dentro dos parênteses após Fn
) e deve retornar um u32
(especificado após o ->
).
Nota: As funções implementam todas as três traits
Fn
também. Se o que queremos fazer não exige a captura de um valor do ambiente, podemos usar uma função em vez de um closure, onde precisamos de algo que implemente uma traitFn
.
O campo value
é do tipo Option<u32>
. Antes de executarmos o closure, o value
será None
. Quando o código que usa Cacher
pede o result (resultado) do closure, o
Cacher
executará o closure naquele momento e armazenará o resultado dentro
de um variante Some
no campo value
. Então, se o código solicitar o resultado
do closure novamente, em vez de executar o closure novamente, o Cacher
retornará
o resultado mantido na variante Some
.
A lógica em torno do campo value
que acabamos de descrever é definida
na Listagem 13-10:
Nome do arquivo: src/main.rs
# struct Cacher<T>
# where T: Fn(u32) -> u32
# {
# calculation: T,
# value: Option<u32>,
# }
#
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}
Lista 13-10: A lógica de armazenamento em cache do Cacher
Queremos que o Cacher
gerencie os valores dos campos da estrutura, em vez de
permitir que o código de chamada potencialmente altere os valores nesses campos
diretamente, que esses campos sejam privados.
A função Cacher::new
usa um parâmetro genérico T
, que definimos como tendo o
mesmo trait vinculado à estrutura do Cacher
. Então Cacher::new
retorna uma
instância do Cacher
que contém o closure especificado no campo calculation
e
um valor None
no campo value
, porque ainda não o executamos.
Quando o código de chamada precisa do resultado da avaliação do closure, em vez
de chamar diretamente o closure, ele chamará o método value
. Este método verifica
se já temos um valor resultante em self.value
em um Some
; se tivermos, ele
retornará o valor dentro de Some
sem executar o closure novamente.
Se self.value
for None
, o código chama o closure armazenado em
self.calculation
, salva o resultado em self.value
para uso futuro, e
retorna o valor também.
A Listagem 13-11 mostra como podemos usar essa estrutura do Cacher
na
função generate_workout
da Listagem 13-6:
Nome do arquivo: src/main.rs
# use std::thread;
# use std::time::Duration;
#
# struct Cacher<T>
# where T: Fn(u32) -> u32
# {
# calculation: T,
# value: Option<u32>,
# }
#
# impl<T> Cacher<T>
# where T: Fn(u32) -> u32
# {
# fn new(calculation: T) -> Cacher<T> {
# Cacher {
# calculation,
# value: None,
# }
# }
#
# fn value(&mut self, arg: u32) -> u32 {
# match self.value {
# Some(v) => v,
# None => {
# let v = (self.calculation)(arg);
# self.value = Some(v);
# v
# },
# }
# }
# }
#
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_result.value(intensity)
);
println!(
"Next, do {} situps!",
expensive_result.value(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.value(intensity)
);
}
}
}
Lista 13-11: Usando o Cacher
na função generate_workout
para abstrair a lógica de armazenamento em cache
Em vez de salvar o closure em uma variável diretamente, salvamos uma nova instância
do Cacher
que mantém o closure. Então, em cada lugar que queremos o resultado,
chamamos o método value
na instância do Cacher
. Podemos chamar o método value
quantas vezes quisermos, ou não chamá-lo nunca, e o cálculo caro será executado
no máximo uma vez.
Tente executar este programa com a função main
da Lista 13-2. Mude os valores
nas variáveis simulated_user_specified_value
e simulated_random_number
para
verificar que em todos os casos nos vários blocos if
e else
,
calculating slowly...
apareça apenas uma vez e somente quando necessário. O Cacher
cuida da lógica necessária para garantir que não estamos chamando o cálculo caro mais
do que precisamos, para que generate_workout
possa se concentrar na lógica de negócios.
O armazenamento em cache de valores é um comportamento geralmente útil que podemos
usar em outras partes do nosso código com closures diferentes. No entanto, existem
dois problemas com a implementação atual do Cacher
que dificultariam sua
reutilização em diferentes contextos.
O primeiro problema é que uma instância do Cacher
assume que sempre obterá o
mesmo valor para o parâmetro arg
no método value
. Ou seja, este teste do
Cacher
falhará:
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2);
}
Este teste cria uma nova instância do Cacher
com um closure que retorna o valor
passado para ele. Chamamos o método value
nesta instância do Cacher
com um
valor arg
de 1 e, em seguida, um valor arg
de 2, e esperamos que a chamada
para value
com o valor arg
de 2 deva retornar 2)
Execute este teste com a implementação do Cacher
na Lista 13-9 e na Lista 13-10,
e o teste falhará no assert_eq!
com esta mensagem:
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `2`', src/main.rs
O problema é que, na primeira vez em que chamamos c.value
com 1, a instância do
Cacher
salvou Some(1)
em self.value
. Depois disso, não importa o que passamos
para o método value
, ele sempre retornará 1.
Tente modificar o Cacher
para conter um mapa de hash em vez de um único valor.
As chaves do mapa de hash serão os valores arg
que são passados, e os valores
do mapa de hash serão o resultado de chamar o closure dessa chave. Em vez de
verificar se o self.value
possui diretamente o valor Some
ou None
, a função
value
procurará o arg
no mapa de hash e retornará o valor, se estiver presente.
Se não estiver presente, o Cacher
chamará o closure e salvará o valor resultante
no mapa de hash associado ao seu valor arg
.
O segundo problema com a implementação atual do Cacher
é que ele aceita apenas
closures que pegam um parâmetro do tipo u32
e retornam um u32
. Podemos querer
armazenar em cache os resultados de closures que pegam uma fatia de string e retornam
valores usize
, por exemplo. Para corrigir esse problema, tente introduzir parâmetros
mais genéricos para aumentar a flexibilidade da funcionalidade do Cacher
.
No exemplo do gerador de treinos, usamos apenas closures como funções anônimas inline. No entanto, os closures têm um recurso adicional que as funções não têm: eles podem capturar seu ambiente e acessar variáveis do escopo em que estão definidos.
A Lista 13-12 tem um exemplo de um closure armazenado na variável equal_to_x
que usa a variável x
do ambiente circundante do closure:
Nome do arquivo: src/main.rs
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
Listagem 13-12: Exemplo de closure que se refere a uma variável em seu escopo anexo (acerca)
Aqui, embora x
não seja um dos parâmetros de equal_to_x
, o closure equal_to_x
pode usar a variável x
que é definida no mesmo escopo em que equal_to_x
é definido.
Não podemos fazer o mesmo com funções; se tentarmos o exemplo a seguir, nosso código não será compilado:
Nome do arquivo: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
Recebemos o error:
error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
--> src/main.rs
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
| ^
O compilador até nos lembra que isso só funciona com closures!
Quando um closure captura um valor de seu ambiente, ele usa memória para armazenar os valores para uso no corpo do closure. Esse uso de memória é uma sobrecarga que não queremos pagar nos casos mais comuns em que queremos executar código que não captura seu ambiente. Como as funções nunca podem capturar seu ambiente, a definição e o uso de funções nunca terão essa sobrecarga.
Os closures podem capturar valores de seu ambiente de três maneiras, que mapeiam
diretamente as três maneiras pelas quais uma função pode assumir um parâmetro:
ownership, tomar borrow mutuamente e tomar borrow imutáveis. Estes são
codificados nas três traits Fn
da seguinte maneira:
FnOnce
consome as variáveis que captura do seu escopo anexo, conhecido como environment (ambiente) do closure. Para consumir as variáveis capturadas, o closure deve ter ownership dessas variáveis e movê-las para o closure quando é definido. A parteOnce
do nome representa o fato de que o closure não pode ter ownership das mesmas variáveis mais de uma vez, para que ele possa ser chamado apenas uma vez.FnMut
pode mudar o ambiente porque borrows valores mutuamente.Fn
borrows valores imutáveis do ambiente.
Quando você cria um closure, Rust infere qual trait usar com base em
como o closure usa os valores do ambiente. Todos os closures implementam o FnOnce
porque todos podem ser chamados pelo menos uma vez. Closures que não movem as
variáveis capturadas também implementam FnMut
, e closures que não precisam
de acesso mutável às variáveis capturadas também implementam Fn
. Na Listagem 13-12, o
closure equal_to_x
empresta imutável x
(para que equal_to_x
tenha a
trait Fn
) porque o corpo do closure precisa apenas ler o valor em x
.
Se você deseja forçar o closure a ter ownership dos valores que ele usa no ambiente,
pode usar a palavra-chave move
antes da lista de parâmetros. Essa técnica é útil
principalmente ao passar um closure para uma nova thread para mover os dados, para
que sejam de ownership da nova thread.
Teremos mais exemplos de closure move
no capítulo 16 quando falarmos sobre
concorrência. Por enquanto, aqui está o código da Lista 13-12 com a palavra-chave
move
adicionada à definição de closure e usando vetores em vez de números
inteiros, porque os números inteiros podem ser copiados em vez de movidos;
observe que esse código ainda não será compilado.
Nome do arquivo: src/main.rs
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
Podemos receber os seguintes erros:
error[E0382]: use of moved value: `x`
--> src/main.rs:6:40
|
4 | let equal_to_x = move |z| z == x;
| -------- value moved (into closure) here
5 |
6 | println!("can't use x here: {:?}", x);
| ^ value used here after move
|
= note: move occurs because `x` has type `std::vec::Vec<i32>`, which does not
implement the `Copy` trait
O valor x
é movido para o closure quando o closure é definido, porque adicionamos
a palavra-chave move
. O closure tem o ownership de x
, e main
não pode mais
usar x
na declaração println!
. A remoção de println!
irá corrigir este exemplo.
Na maioria das vezes, ao especificar um dos limites de trait Fn
, você pode
começar com Fn
e o compilador informará se você precisa de FnMut
ou FnOnce
com base no que acontece no corpo do closure.
Para ilustrar situações em que closures capazes de capturar seu ambiente são úteis como parâmetros de função, vamos para o próximo tópico: iteradores.