简短的答案:为了获得最大的灵活性,您可以将回调存储为盒装FnMut
对象,并且回调设置器对回调类型具有通用性。答案的最后一个示例中显示了此代码。有关更详细的说明,请继续阅读。
“函数指针”:回调为 fn
问题中与C ++代码最接近的等效项是将回调声明为一种fn
类型。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 p = Processor {
callback: simple_callback,
};
p.process_events();
}
该代码可以扩展为包括一个Option<Box<Any>>
用于保存与该功能关联的“用户数据”的代码。即使这样,它也不是惯用的Rust。将数据与函数关联的Rust方法是在匿名闭包中捕获数据,就像在现代C ++中一样。由于闭包不是fn
,set_callback
因此需要接受其他类型的函数对象。
回调作为通用函数对象
在具有相同调用签名的Rust和C ++闭包中,大小不同,以适应它们可能捕获的不同值。此外,每个闭包定义都会为闭包的值生成一个唯一的匿名类型。由于这些限制,该结构无法命名其callback
字段的类型,也不能使用别名。
在不引用具体类型的情况下将闭包嵌入struct字段的一种方法是使struct泛型。该结构将为传递给它的具体函数或闭包自动调整其大小和回调类型:
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)
。因此,处理器不需要userdata
伴随回调。调用方提供的闭包set_callback
将自动从其环境中捕获所需的数据,并在调用时使其可用。
但是FnMut
,为什么不Fn
呢?由于闭包保留捕获的值,因此在调用闭包时必须应用Rust的常规变异规则。根据闭包使用它们所拥有的值的方式,将它们分为三个家族,每个家族都标有一个特征:
Fn
是仅读取数据的闭包,可以安全地多次调用(可能从多个线程调用)。以上两个闭包都是Fn
。
FnMut
是用于修改数据(例如通过写入捕获的mut
变量)的闭包。它们也可能被多次调用,但不能并行调用。(FnMut
从多个线程调用闭包将导致数据争用,因此只能在保护互斥锁的情况下完成。)闭包对象必须被调用方声明为可变的。
FnOnce
是消耗一些捕获数据的闭包,例如,通过将捕获的值移动到具有其所有权的函数。顾名思义,它们只能被调用一次,调用者必须拥有它们。
当为接受闭包的对象的类型指定一个特征绑定时,有些反直觉FnOnce
实际上是最宽松的。声明通用回调类型必须满足该FnOnce
特性意味着它将接受字面上的任何闭包。但这附带了价格:这意味着持有人只能叫一次。由于process_events()
可能会选择多次调用回调,并且由于方法本身可能会被多次调用,因此下一个允许的范围是FnMut
。请注意,我们必须将其标记process_events
为mutating self
。
非泛型回调:函数特征对象
即使回调的通用实现非常高效,但它具有严重的接口限制。它要求每个Processor
实例都使用具体的回调类型进行参数化,这意味着单个实例Processor
只能处理单个回调类型。鉴于每个闭包具有不同的类型,泛型Processor
无法处理,proc.set_callback(|| println!("hello"))
后跟proc.set_callback(|| println!("world"))
。扩展该结构以支持两个回调字段将需要将整个结构参数化为两种类型,随着回调数量的增加,它们将很快变得笨拙。如果需要动态地增加回调的数量(例如,实现一个add_callback
维护不同回调向量的函数),则无法添加更多的类型参数。
要删除类型参数,我们可以利用trait对象(Rust的功能),该特性允许基于特征自动创建动态接口。这有时被称为类型擦除,并且是C ++ [1] [2]中的一种流行技术,不要与Java和FP语言对该术语的使用有所不同。熟悉C ++的读者会认识到实现的闭包Fn
和Fn
trait对象之间的区别等同std::function
于C ++中通用函数对象和值之间的区别。
通过与&
运算符借用对象并将其强制转换或强制为对特定特征的引用,可以创建特征对象。在这种情况下,由于Processor
需要拥有回调对象,因此我们不能使用借用,而必须将回调存储在分配给堆的功能Box<dyn Trait>
(在Rust上等效于std::unique_ptr
),在功能上等效于trait对象。
如果Processor
存储Box<dyn FnMut()>
,则不再需要泛型,但是该set_callback
方法现在c
通过impl Trait
参数接受泛型。这样,它可以接受任何类型的可调用对象(包括带状态的闭包),并在将其存储在中之前正确装箱Processor
。通用参数set_callback
不会限制处理器接受的回调类型,因为接受的回调的类型与存储在Processor
结构中的类型是分离的。
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();
}
盒装封闭中引用的生命周期
接受'static
的c
参数类型的生存期限制set_callback
是一种简单的方法,可以使编译器确信包含在中的引用(c
可能是引用其环境的闭包,仅引用全局值),因此在使用过程中始终保持有效。打回来。但是静态绑定也非常笨拙:虽然它接受拥有对象的闭包就好了(我们在上面通过闭包来确保了这一点move
),但它拒绝引用本地环境的闭包,即使它们仅引用了那些超过处理器的寿命,实际上是安全的。
由于只要处理器处于活动状态,我们只需要使回调保持活动状态,就应该尝试将其生存期与处理器的生存期联系起来,这比严格限制'static
。但是,如果我们仅从中删除'static
有效期限制set_callback
,它将不再进行编译。这是因为set_callback
创建一个新框并将其分配给callback
定义为的字段Box<dyn FnMut()>
。由于该定义未为装箱的trait对象指定生存期'static
,因此该分配将有效地延长生存期(从回调的未命名任意生存期到'static
),这是不允许的。解决方法是为处理器提供一个明确的生存期,并将该生存期与框内的引用以及由以下方法接收的回调中的引用相关联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);
}
}
通过明确指定这些生命周期,不再需要使用'static
。现在,闭包可以引用本地s
对象,也就是不必再引用move
,只要将的定义s
放在的定义之前,p
以确保字符串的寿命超过处理器。
CB
必须'static
在最后一个示例中出现?