Rust中的习惯性callback

在C / C ++中,我通常使用普通的函数指针进行callback,也许会传递一个void* userdata参数。 像这样的东西:

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

在Rust里做这件事的惯用方法是什么? 具体来说,我的setCallback()函数应该mCallback什么types, mCallback应该是什么types? 它应该采取一个Fn ? 也许FnMut ? 我把它保存Boxed ? 一个例子将是惊人的。

简短的回答:为了获得最大的灵活性,您可以将callback存储为装箱的FnMut对象,callback设置器通用callbacktypes。 这个代码显示在答案的最后一个例子中。 有关更详细的解释,请继续阅读。

“函数指针”:callback为fn

问题中最接近的C ++代码将声明callback为fntypes。 fn封装了由fn关键字定义的函数,就像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! } 

此代码可以扩展为包含一个Option<Box<Any>>来保存与该函数关联的“用户数据”。 即便如此,这也不会是惯用的锈。 用数据调用函数的Rust方法是接受一个闭包作为callback – 就像现代C ++一样。

callback作为通用函数对象

在Rust和C ++中,具有相同调用签名的闭包具有不同的大小,以适应它们存储在闭包对象中的不同大小的捕获值。 此外,每个闭包站点都会生成一个独特的匿名types,它是编译时闭包对象的types。 由于这些约束,结构体不能通过名称或types别名引用callbacktypes。

在结构中拥有一个闭包而不引用具体types的一种方法是使结构通用 。 该结构将自动调整其大小和callbacktypes的具体function或封闭你传递给它:

 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(); } 

和以前一样,新的callback定义将能够接受用fn定义的顶层函数,但是这个函数也会接受闭包。 || println!("hello world!") ,以及捕获值的闭包,例如|| println!("{}", somevar) || println!("{}", somevar) 。 因此,闭包不需要单独的userdata参数; 它可以简单地从其环境中捕获数据,并在调用时可用。

但是与FnMut的交易是FnMut ,为什么不是Fn ? 由于闭包持有捕获的值,所以Rust在它们上执行相同的规则,使其在其他容器对象上执行。 根据封闭对价值的影响,他们分为三个家族,每个家族都有一个特点:

  • Fn是只读取数据的闭包,可以安全地多次调用,可能来自多个线程。 上述两个封闭都是Fn
  • FnMut是closures修改数据,例如通过写入捕获的mutvariables。 他们也可能被称为多次,但不是并行的。 (从multithreading调用FnMut闭包会导致数据竞争,所以只能通过互斥体的保护来完成)。闭包对象必须声明为可调。
  • FnOnce使用它们捕获的数据的闭包,例如通过将其移动到拥有它们的函数。 顾名思义,这些可能只被调用一次,而调用者必须拥有它们。

有点反直觉地指出,当为一个接受闭包的对象的types指定一个特征时, FnOnce实际上是最宽松的一个。 声明一个通用的callbacktypes必须满足FnOnce特性意味着它将从字面上接受任何闭包。 但是,这是一个价格:这意味着持有人只能被称为一次。 由于process_events()可以select多次调用callbackFnMut ,并且由于方法本身可能被多次调用,所以下一个最宽松的边界是FnMut 。 请注意,我们必须将process_events标记为mutate self

非通用callback:函数特质对象

尽pipecallback的通用实现是非常有效的,但它有严重的接口限制。 它要求每个Processor实例用一个具体的callbacktypes进行参数化,这意味着一个Processor只能处理一个callbacktypes。 假设每个闭包都有不同的types,那么通用Processor无法处理proc.set_callback(|| println!("hello"))接着是proc.set_callback(|| println!("world")) 。 将结构扩展为支持两个callback字段将需要将整个结构参数化为两种types,随着callback数量的增加,这将迅速变得笨拙。 如果callback数量需要是dynamic的,添加更多的types参数将不起作用,例如实现一个add_callback函数来维护一个不同callback的向量。

为了移除types参数,我们可以利用特征对象 ,Rust的特性允许基于特征自动创builddynamic接口。 这有时被称为types擦除,并且是C ++ [1] [2]中stream行的技术,不会与Java和FP语言在术语上有所不同的使用相混淆。 在closures的情况下,实现Fn的对象和Fn特征对象之间的区别等同于C ++中的一般函数对象和std::function对象之间的区别。

特征对象是通过借助&操作符借用一个对象并将它强制转换为对特定特征的引用来创build的。 在这种情况下,由于Processor需要拥有callback对象,所以我们不能使用借用,但是必须将callback存储在一个堆分配的Box<Trait>std::unique_ptr的Rust等价物)中,这在function上等同于一个特性目的。

如果Processor存储Box<FnMut()> ,它不再需要是通用的,但set_callback 方法现在是通用的,所以它可以正确地将任何可调用的方框打开,然后将方框存储到Processor 。 callback可以是任何种类,只要它不消耗捕获的值。 通用set_callback不会产生上面讨论的限制,因为它不会影响存储在结构中的数据的接口。

 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(); }