Resposta curta: para flexibilidade máxima, você pode armazenar o retorno de chamada como um FnMut
objeto em caixa , com o setter de retorno de chamada genérico no tipo de retorno de chamada. O código para isso é mostrado no último exemplo da resposta. Para uma explicação mais detalhada, continue lendo.
"Ponteiros de função": retornos de chamada como fn
O equivalente mais próximo do código C ++ na questão seria declarar o retorno de chamada como um fn
tipo. fn
encapsula funções definidas pela fn
palavra - chave, muito parecido com os ponteiros de função do C ++:
type Callback = fn();
struct Processor {
callback: Callback,
}
impl Processor {
fn set_callback(&mut self, c: Callback) {
self.callback = c;
}
fn process_events(&self) {
(self.callback)();
}
}
fn simple_callback() {
println!("hello world!");
}
fn main() {
let p = Processor {
callback: simple_callback,
};
p.process_events();
}
Esse código pode ser estendido para incluir um Option<Box<Any>>
para conter os "dados do usuário" associados à função. Mesmo assim, não seria Rust idiomático. A maneira do Rust de associar dados a uma função é capturá-los em um fechamento anônimo , assim como no C ++ moderno. Como os fechamentos não são fn
, set_callback
ele precisará aceitar outros tipos de objetos de função.
Callbacks como objetos de função genéricos
Em ambos os fechamentos Rust e C ++ com a mesma assinatura de chamada vêm em tamanhos diferentes para acomodar os diferentes valores que eles podem capturar. Além disso, cada definição de fechamento gera um tipo anônimo exclusivo para o valor do fechamento. Devido a essas restrições, a estrutura não pode nomear o tipo de seu callback
campo, nem pode usar um alias.
Uma maneira de inserir um encerramento no campo de estrutura sem se referir a um tipo concreto é tornando a estrutura genérica . A estrutura irá adaptar automaticamente seu tamanho e o tipo de retorno de chamada para a função concreta ou fechamento que você passar para ela:
struct Processor<CB>
where
CB: FnMut(),
{
callback: CB,
}
impl<CB> Processor<CB>
where
CB: FnMut(),
{
fn set_callback(&mut self, c: CB) {
self.callback = c;
}
fn process_events(&mut self) {
(self.callback)();
}
}
fn main() {
let s = "world!".to_string();
let callback = || println!("hello {}", s);
let mut p = Processor { callback: callback };
p.process_events();
}
Como antes, a nova definição de retorno de chamada será capaz de aceitar funções de nível superior definidas com fn
, mas esta também aceitará encerramentos como || println!("hello world!")
, bem como encerramentos que capturem valores, como || println!("{}", somevar)
. Por isso, o processador não precisa userdata
acompanhar o retorno de chamada; o encerramento fornecido pelo chamador de set_callback
capturará automaticamente os dados de que precisa de seu ambiente e os terá disponíveis quando chamado.
Mas qual é o problema FnMut
, por que não apenas Fn
? Uma vez que os encerramentos mantêm valores capturados, as regras de mutação usuais de Rust devem ser aplicadas ao chamar o encerramento. Dependendo do que os fechamentos fazem com os valores que mantêm, eles são agrupados em três famílias, cada uma marcada com uma característica:
Fn
são encerramentos que apenas leem dados e podem ser chamados com segurança várias vezes, possivelmente a partir de vários threads. Ambos os fechamentos acima são Fn
.
FnMut
são fechamentos que modificam dados, por exemplo, gravando em uma mut
variável capturada . Eles também podem ser chamados várias vezes, mas não em paralelo. (Chamar um FnMut
encerramento de vários threads levaria a uma disputa de dados, portanto, isso só pode ser feito com a proteção de um mutex.) O objeto de encerramento deve ser declarado mutável pelo chamador.
FnOnce
são fechamentos que consomem alguns dos dados que capturam, por exemplo, movendo um valor capturado para uma função que assume sua propriedade. Como o nome indica, eles podem ser chamados apenas uma vez e o chamador deve ser o proprietário deles.
Um tanto contra-intuitivamente, ao especificar um traço ligado ao tipo de um objeto que aceita um fechamento, FnOnce
é na verdade o mais permissivo. Declarar que um tipo de retorno de chamada genérico deve satisfazer a FnOnce
característica significa que ele aceitará literalmente qualquer encerramento. Mas isso tem um preço: significa que o titular só pode fazer a chamada uma vez. Como process_events()
pode optar por invocar o retorno de chamada várias vezes e como o próprio método pode ser chamado mais de uma vez, o próximo limite mais permissivo é FnMut
. Observe que tivemos que marcar process_events
como mutante self
.
Callbacks não genéricos: objetos de característica de função
Embora a implementação genérica do retorno de chamada seja extremamente eficiente, ela possui sérias limitações de interface. Exige que cada Processor
instância seja parametrizada com um tipo de retorno de chamada concreto, o que significa que um só Processor
pode lidar com um único tipo de retorno de chamada. Dado que cada closure possui um tipo distinto, o genérico Processor
não pode manipular proc.set_callback(|| println!("hello"))
seguido por proc.set_callback(|| println!("world"))
. Estender a estrutura para suportar dois campos de retorno de chamada exigiria que toda a estrutura fosse parametrizada em dois tipos, o que rapidamente se tornaria difícil de controlar conforme o número de retornos de chamada aumentasse. Adicionar mais parâmetros de tipo não funcionaria se o número de callbacks precisasse ser dinâmico, por exemplo, para implementar uma add_callback
função que mantém um vetor de callbacks diferentes.
Para remover o parâmetro de tipo, podemos tirar proveito de objetos de características , o recurso do Rust que permite a criação automática de interfaces dinâmicas com base em características. Isso às vezes é referido como eliminação de tipo e é uma técnica popular em C ++ [1] [2] , que não deve ser confundida com o uso um pouco diferente do termo nas linguagens Java e FP. Os leitores familiarizados com C ++ reconhecerão a distinção entre um fechamento que implementa Fn
e um Fn
objeto de característica como equivalente à distinção entre objetos de função geral e std::function
valores em C ++.
Um objeto de característica é criado pegando emprestado um objeto com o &
operador e lançando-o ou coagindo-o a uma referência à característica específica. Nesse caso, uma vez que Processor
precisa possuir o objeto de retorno de chamada, não podemos usar o empréstimo, mas devemos armazenar o retorno de chamada em um heap alocado Box<dyn Trait>
(o equivalente em Rust de std::unique_ptr
), que é funcionalmente equivalente a um objeto de característica.
Se Processor
armazena Box<dyn FnMut()>
, ele não precisa mais ser genérico, mas o set_callback
método agora aceita um genérico c
por meio de um impl Trait
argumento . Como tal, ele pode aceitar qualquer tipo de chamada, incluindo fechamentos com estado, e encaixotá-lo adequadamente antes de armazená-lo no Processor
. O argumento genérico para set_callback
não limita o tipo de retorno de chamada que o processador aceita, pois o tipo de retorno de chamada aceito é desacoplado do tipo armazenado na Processor
estrutura.
struct Processor {
callback: Box<dyn FnMut()>,
}
impl Processor {
fn set_callback(&mut self, c: impl FnMut() + 'static) {
self.callback = Box::new(c);
}
fn process_events(&mut self) {
(self.callback)();
}
}
fn simple_callback() {
println!("hello");
}
fn main() {
let mut p = Processor {
callback: Box::new(simple_callback),
};
p.process_events();
let s = "world!".to_string();
let callback2 = move || println!("hello {}", s);
p.set_callback(callback2);
p.process_events();
}
Tempo de vida das referências dentro de fechos em caixas
O 'static
tempo de vida limitado ao tipo de c
argumento aceito por set_callback
é uma maneira simples de convencer o compilador de que as referências contidas em c
, que pode ser um fechamento que se refere ao seu ambiente, se referem apenas a valores globais e, portanto, permanecerão válidas durante o uso do ligue de volta. Mas o limite estático também é muito pesado: embora aceite fechamentos que possuem objetos muito bem (o que garantimos acima ao fazer o fechamento move
), ele rejeita fechamentos que se referem ao ambiente local, mesmo quando eles se referem apenas a valores que sobreviveria ao processador e seria de fato seguro.
Como precisamos apenas dos callbacks ativos enquanto o processador estiver ativo, devemos tentar vincular seu tempo de vida ao do processador, que é um limite menos estrito do que 'static
. Mas se apenas removermos o 'static
limite de vida set_callback
, ele não será mais compilado. Isso ocorre porque set_callback
cria uma nova caixa e a atribui ao callback
campo definido como Box<dyn FnMut()>
. Como a definição não especifica um tempo de vida para o objeto de característica em caixa, 'static
está implícito, e a atribuição efetivamente ampliaria o tempo de vida (de um tempo de vida arbitrário sem nome do retorno de chamada para 'static
), o que não é permitido. A correção é fornecer um tempo de vida explícito para o processador e vincular esse tempo de vida às referências na caixa e às referências no retorno de chamada recebido por set_callback
:
struct Processor<'a> {
callback: Box<dyn FnMut() + 'a>,
}
impl<'a> Processor<'a> {
fn set_callback(&mut self, c: impl FnMut() + 'a) {
self.callback = Box::new(c);
}
}
Com essas vidas tornadas explícitas, não é mais necessário usar 'static
. O fechamento agora pode se referir ao s
objeto local , ou seja, não precisa mais ser move
, desde que a definição de s
seja colocada antes da definição de p
para garantir que a string sobreviva ao processador.
CB
tem que estar'static
no exemplo final?