callback rust

Callbacks idiomáticos en Rust



(1)

Respuesta corta: para obtener la máxima flexibilidad, puede almacenar la devolución de llamada como un objeto FnMut caja, con el FnMut 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 en la respuesta. Para 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 tipo fn . fn encapsula funciones definidas por la palabra clave fn , 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 mut p = Processor { callback: simple_callback }; p.process_events(); // hello world! }

Este código podría extenderse para incluir una Option<Box<Any>> para contener los "datos de usuario" asociados con la función. Aun así, no sería Rid idiomático. La forma Rust de asociar datos con una función es capturarlos en un cierre anónimo, al igual que en C ++ moderno. Como los cierres no son fn , set_callback deberá aceptar otros tipos de objetos de función.

Callbacks como objetos de función genéricos

Tanto en los cierres Rust como en C ++ con la misma firma de llamada vienen en diferentes tamaños para acomodar diferentes tamaños de los valores capturados que almacenan en el objeto de cierre. Además, cada sitio de cierre genera un tipo anónimo distinto que es el tipo del objeto de cierre en tiempo de compilación. Debido a estas restricciones, la estructura no puede hacer referencia al tipo de devolución de llamada por nombre o un alias de tipo.

Una forma de poseer un cierre en la estructura sin referirse a un tipo concreto es haciendo 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 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!") || println!("hello world!") , así como cierres que capturan valores, como || println!("{}", somevar) || println!("{}", somevar) . Debido a esto, el cierre no necesita un argumento separado de datos de userdata ; simplemente puede capturar los datos de su entorno y estará disponible cuando se llame.

Pero, ¿cuál es el trato con el FnMut , por qué no solo Fn ? Dado que los cierres contienen valores capturados, Rust aplica en ellos las mismas reglas que en otros objetos de contenedor. Dependiendo de lo que hagan los cierres con los valores que poseen, 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 múltiples subprocesos. Los dos cierres anteriores son Fn .
  • FnMut son cierres que modifican datos, por ejemplo, escribiendo en una variable mut capturada. También pueden llamarse varias veces, pero no en paralelo. (Llamar a un cierre de FnMut desde múltiples subprocesos conduciría a una carrera de datos, por lo que solo se puede hacer con la protección de un mutex). El llamante debe declarar el objeto de cierre mutable.
  • FnOnce son cierres que consumen los datos que capturan, por ejemplo, moviéndolos a una función que los posee. Como su nombre lo indica, estos pueden llamarse solo una vez, y la persona que llama debe ser el propietario.

Algo contradictorio, cuando se especifica un rasgo limitado para el 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 rasgo FnOnce 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 como se puede llamar al método en sí más de una vez, el siguiente límite más permisivo es FnMut . Tenga en cuenta que tuvimos que marcar process_events como self mutante.

Callbacks no genéricos: objetos de rasgos 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 instancia del Processor se parametrice con un tipo de devolución de llamada concreto, lo que significa que un solo Processor solo puede tratar con un solo tipo de devolución de llamada. Dado que cada cierre tiene un tipo distinto, el Processor genérico no puede manejar proc.set_callback(|| println!("hello")) seguido de proc.set_callback(|| println!("world")) . Extender la estructura para admitir dos campos de devolución de llamada requeriría que la estructura completa se parametrice a 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 tuviera que ser dinámico, por ejemplo, para implementar una función add_callback que mantenga un vector de devoluciones de llamada diferentes.

Para eliminar el parámetro de tipo, podemos aprovechar los objetos de rasgos , la característica de Rust que permite la creación automática de interfaces dinámicas basadas en rasgos. Esto a veces se conoce como borrado de tipo y es una técnica popular en C ++ [1] [2] , que no debe confundirse con el uso algo diferente del término de los lenguajes Java y FP. Los lectores familiarizados con C ++ reconocerán la distinción entre un cierre que implementa Fn y un objeto de rasgo Fn como equivalente a la distinción entre objetos de función general y valores de std::function en C ++.

Un objeto de rasgo se crea tomando prestado un objeto con el operador & y convirtiéndolo o coaccionándolo a una referencia al rasgo específico. En este caso, dado que el Processor necesita ser el propietario del objeto de devolución de llamada, no podemos usar préstamos, sino que debemos almacenar la devolución de llamada en un Box<Trait> asignado por el montón (el equivalente Rust de std::unique_ptr ), que es funcionalmente equivalente a un rasgo objeto.

Si el Processor almacena Box<FnMut()> , ya no necesita ser genérico, pero el método set_callback ahora es genérico, por lo que puede set_callback correctamente lo que se le pueda set_callback antes de almacenar el box en el Processor . La devolución de llamada puede ser de cualquier tipo siempre que no consuma los valores capturados. set_callback siendo genérico no incurre en las limitaciones discutidas anteriormente, ya que no afecta la interfaz de los datos almacenados en la estructura.

struct Processor { callback: Box<FnMut()>, } impl Processor { fn set_callback<CB: ''static + FnMut()>(&mut self, c: CB) { 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(); }

En C / C ++ normalmente haría devoluciones de llamada con un puntero de función simple, tal vez pasando también un parámetro void* userdata . Algo como esto:

typedef void (*Callback)(); class Processor { public: void setCallback(Callback c) { mCallback = c; } void processEvents() { for (...) { ... mCallback(); } } private: Callback mCallback; };

¿Cuál es la forma idiomática de hacer esto en Rust? Específicamente, ¿qué tipos debería setCallback() mi función setCallback() y qué tipo debería ser mCallback ? ¿Debería tomar un Fn ? Tal vez FnMut ? ¿Lo guardo en Boxed ? Un ejemplo sería asombroso.