Respuesta corta: para una máxima flexibilidad, puede almacenar la devolución de llamada como un FnMut
objeto en caja , con el establecedor de devolución de llamada genérico en el tipo de devolución de llamada. El código para esto se muestra en el último ejemplo de la respuesta. Para obtener una explicación más detallada, sigue leyendo.
"Punteros de función": devoluciones de llamada como fn
El equivalente más cercano del código C ++ en la pregunta sería declarar la devolución de llamada como un fn
tipo. fn
encapsula funciones definidas por la fn
palabra clave, al igual que los punteros de función de 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();
}
Este código podría ampliarse para incluir un Option<Box<Any>>
para contener los "datos de usuario" asociados con la función. Aun así, no sería idiomático Rust. La forma de Rust de asociar datos con una función es capturarlos en un cierre anónimo , al igual que en C ++ moderno. Dado que los cierres no lo son fn
, set_callback
deberá aceptar otros tipos de objetos de función.
Devoluciones de llamada como objetos de función genéricos
Tanto en Rust como en C ++, los cierres con la misma firma de llamada vienen en diferentes tamaños para adaptarse a los diferentes valores que pueden capturar. Además, cada definición de cierre genera un tipo anónimo único para el valor del cierre. Debido a estas restricciones, la estructura no puede nombrar el tipo de su callback
campo, ni puede usar un alias.
Una forma de incrustar un cierre en el campo de estructura sin hacer referencia a un tipo concreto es hacer que la estructura sea genérica . La estructura adaptará automáticamente su tamaño y el tipo de devolución de llamada para la función concreta o el cierre que le pase:
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, la nueva definición de devolución de llamada podrá aceptar funciones de nivel superior definidas con fn
, pero esta también aceptará cierres como || println!("hello world!")
, así como cierres que capturan valores, como || println!("{}", somevar)
. Debido a esto, el procesador no necesita userdata
acompañar la devolución de llamada; el cierre proporcionado por la persona que llamaset_callback
capturará automáticamente los datos que necesita de su entorno y los tendrá disponibles cuando se invoca.
Pero, ¿cuál es el problema con FnMut
, por qué no solo Fn
? Dado que los cierres contienen valores capturados, las reglas de mutación habituales de Rust deben aplicarse al llamar al cierre. Dependiendo de lo que hagan los cierres con los valores que tengan, se agrupan en tres familias, cada una marcada con un rasgo:
Fn
son cierres que solo leen datos y se pueden llamar de forma segura varias veces, posiblemente desde varios subprocesos. Ambos cierres anteriores son Fn
.
FnMut
son cierres que modifican datos, por ejemplo, escribiendo en una mut
variable capturada . También se pueden llamar varias veces, pero no en paralelo. (Llamar a un FnMut
cierre desde varios subprocesos conduciría a una carrera de datos, por lo que solo se puede hacer con la protección de un mutex). El llamador debe declarar el objeto de cierre como mutable.
FnOnce
son cierres que consumen parte de los datos que capturan, por ejemplo, al mover un valor capturado a una función que toma su propiedad. Como su nombre lo indica, solo se pueden llamar una vez y la persona que llama debe poseerlos.
Algo contrario a la intuición, cuando se especifica un rasgo vinculado al tipo de objeto que acepta un cierre, FnOnce
es en realidad el más permisivo. Declarar que un tipo de devolución de llamada genérico debe satisfacer el FnOnce
rasgo significa que aceptará literalmente cualquier cierre. Pero eso tiene un precio: significa que el titular solo puede llamarlo una vez. Dado que process_events()
puede optar por invocar la devolución de llamada varias veces, y dado que el método en sí puede llamarse más de una vez, el siguiente límite más permisivo es FnMut
. Tenga en cuenta que tuvimos que marcar process_events
como mutante self
.
Devoluciones de llamada no genéricas: objetos de rasgo de función
Aunque la implementación genérica de la devolución de llamada es extremadamente eficiente, tiene serias limitaciones de interfaz. Requiere que cada Processor
instancia esté parametrizada con un tipo de devolución de llamada concreto, lo que significa que una sola Processor
solo puede tratar con un solo tipo de devolución de llamada. Dado que cada cierre tiene un tipo distinto, el genérico Processor
no puede manejar proc.set_callback(|| println!("hello"))
seguido de proc.set_callback(|| println!("world"))
. Extender la estructura para admitir dos campos de devoluciones de llamada requeriría que toda la estructura se parametrice en dos tipos, lo que rápidamente se volvería difícil de manejar a medida que aumenta el número de devoluciones de llamada. Agregar más parámetros de tipo no funcionaría si el número de devoluciones de llamada necesitara ser dinámico, por ejemplo, para implementar una add_callback
función que mantiene un vector de diferentes devoluciones de llamada.
Para eliminar el parámetro de tipo, podemos aprovechar los objetos de rasgo , la característica de Rust que permite la creación automática de interfaces dinámicas basadas en rasgos. Esto a veces se denomina borrado de tipos y es una técnica popular en C ++ [1] [2] , que no debe confundirse con el uso algo diferente del término en los lenguajes Java y FP. Los lectores familiarizados con C ++ reconocerán la distinción entre un cierre que implementa Fn
y un Fn
objeto de rasgo como equivalente a la distinción entre objetos de función general y std::function
valores en C ++.
Un objeto de rasgo se crea tomando prestado un objeto con el &
operador y lanzándolo o forzándolo a hacer una referencia al rasgo específico. En este caso, dado que Processor
necesita poseer el objeto de devolución de llamada, no podemos usar el préstamo, pero debemos almacenar la devolución de llamada en un montón asignado Box<dyn Trait>
(el equivalente de Rust std::unique_ptr
), que es funcionalmente equivalente a un objeto de rasgo.
Si se Processor
almacena Box<dyn FnMut()>
, ya no necesita ser genérico, pero el set_callback
método ahora acepta un genérico a c
través de un impl Trait
argumento . Como tal, puede aceptar cualquier tipo de invocable, incluidos los cierres con estado, y empaquetarlo correctamente antes de almacenarlo en el archivo Processor
. El argumento genérico de set_callback
no limita el tipo de devolución de llamada que acepta el procesador, ya que el tipo de devolución de llamada aceptada está desacoplado del tipo almacenado en la Processor
estructura.
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();
}
Vida útil de las referencias dentro de los cierres en caja
El 'static
límite de duración del tipo de c
argumento aceptado por set_callback
es una forma sencilla de convencer al compilador de que las referencias contenidas en c
, que podría ser un cierre que se refiere a su entorno, solo se refieren a valores globales y, por lo tanto, seguirán siendo válidas durante el uso de la llamar de vuelta. Pero el límite estático también es muy torpe: si bien acepta cierres que poseen objetos bien (lo cual nos hemos asegurado anteriormente al hacer el cierre move
), rechaza los cierres que se refieren al entorno local, incluso cuando solo se refieren a valores que sobrevivirá al procesador y, de hecho, sería seguro.
Como solo necesitamos las devoluciones de llamada activas mientras el procesador esté vivo, deberíamos intentar vincular su vida útil a la del procesador, que es un límite menos estricto que 'static
. Pero si simplemente eliminamos el 'static
límite de por vida set_callback
, ya no se compila. Esto se debe a que set_callback
crea un nuevo cuadro y lo asigna al callback
campo definido como Box<dyn FnMut()>
. Dado que la definición no especifica una vida útil para el objeto de rasgo en caja, 'static
está implícita, y la asignación ampliaría efectivamente la vida útil (desde una vida útil arbitraria sin nombre de la devolución de llamada a 'static
), lo cual no está permitido. La solución es proporcionar una vida útil explícita para el procesador y vincular esa vida útil tanto a las referencias en el cuadro como a las referencias en la devolución de llamada recibida 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);
}
}
Dado que estas vidas se hacen explícitas, ya no es necesario usarlo 'static
. El cierre ahora puede hacer referencia al s
objeto local , es decir, ya no tiene que serlo move
, siempre que la definición de s
se coloque antes de la definición de p
para garantizar que la cadena sobreviva al procesador.
CB
tiene que estar'static
en el ejemplo final?