简短回答:为了最大限度的灵活性,您可以将回调作为一个装箱的
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 Processor<CB> {
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 };
p.process_events();
}
与以前一样,set_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
标记为可变的
self
。
非通用回调:函数特质对象
尽管回调的通用实现非常高效,但它具有严重的接口限制。它要求每个Processor
实例都使用具体的回调类型进行参数化,这意味着单个Processor
只能处理单个回调类型。鉴于每个闭包都具有不同的类型,通用Processor
无法处理proc.set_callback(|| println!("hello"))
后跟proc.set_callback(|| println!("world"))
。扩展结构以支持两个回调字段将需要整个结构被参数化为两种类型,随着回调数量的增加,这将很快变得笨重。如果需要动态地添加更多回调(例如实现维护不同回调向量的add_callback
函数),则添加更多类型参数是行不通的。
为了去除类型参数,我们可以利用Rust的trait objects功能,该功能允许基于特征自动创建动态接口。这有时被称为类型擦除,是C++中的一种流行技术[1][2],不要与Java和FP语言中略有不同的术语使用混淆。熟悉C++的读者将认识到实现Fn
闭包和Fn
特征对象之间的区别相当于C++中一般函数对象和std::function
值之间的区别。
一个特质对象是通过使用
&
运算符借用一个对象并将其强制转换或强制转换为特定特质的引用来创建的。在这种情况下,由于
Processor
需要拥有回调对象,因此我们不能使用借用,而必须将回调存储在堆分配的
Box<dyn Trait>
中(Rust等效于
std::unique_ptr
),它在功能上等同于特质对象。
如果
Processor
存储
Box<dyn FnMut()>
,它就不再需要是泛型的了,但是
set_callback
方法现在通过
impl Trait
argument接受一个泛型
c
。因此,它可以接受任何类型的可调用对象,包括带状态的闭包,并在存储到
Processor
之前正确地将其封装成Box。
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();
}
闭包中引用的生命周期
set_callback
函数接受的 c
参数类型上的 'static
生命周期限制是一种简单的方法,可以让编译器相信 c
中包含的 引用(可能是一个引用其环境的闭包)只引用全局值,并且在整个回调使用期间保持有效。但是静态限制也非常笨重:虽然它可以很好地接受拥有对象的闭包(我们通过使闭包 move
来确保这一点),但它会拒绝引用本地环境的闭包,即使它们只引用超过处理器寿命并且实际上是安全的值。
由于我们只需要在处理器存活期间保持回调函数的存活,因此我们应该尝试将它们的生命周期与处理器的生命周期绑定,这比 'static
更宽松。但是,如果我们仅从 set_callback
中删除 'static
生命周期限制,它将不再编译。这是因为 set_callback
创建一个新的盒子,并将其分配给定义为 Box<dyn FnMut()>
的 callback
字段。由于定义没有为盒装特质对象指定生命周期,因此暗示使用 '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
,只要确保在p
定义之前放置s
的定义以确保字符串存活超过处理器。
CB
必须是“静态”的? - TimmmmBox<FnMut()>
表示Box<FnMut() + 'static>
。大致意思是 "这个被盒装的 trait 对象不包含任何引用,或者它包含的所有引用都比'static
生命周期更长或相等"。这可以防止回调函数通过引用捕获局部变量。 - bluss'static
绑定的详细信息,请参阅单独的博客文章。 - user4815162342