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为fn
types。 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修改数据,例如通过写入捕获的mut
variables。 他们也可能被称为多次,但不是并行的。 (从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(); }