在C/C++中,我通常会用一个普通的函数指针进行回调,可能还会传递一个void* userdata
参数。
typedef void (*Callback)();
class Processor
{
public:
void setCallback(Callback c)
{
mCallback = c;
}
void processEvents()
{
//...
mCallback();
}
private:
Callback mCallback;
};
在Rust中,惯用的方法是什么?具体来说,我的setCallback()
函数应该采用什么类型,mCallback
应该是什么类型?它应该采用Fn
吗?也许是FnMut
?我应该把它保存为Boxed
吗?一个例子会很棒。
4条答案
按热度按时间hpcdzsge1#
简短回答:为了获得最大的灵活性,您可以将回调存储为一个装箱的
FnMut
对象,回调setter为回调类型的泛型。答案中的最后一个示例显示了这方面的代码。有关更详细的解释,请继续阅读。“函数指针”:以
fn
形式回调与问题中的C代码最接近的等价物是将回调声明为
fn
类型。fn
封装了由fn
关键字定义的函数,非常类似于C的函数指针:这段代码可以扩展为包含一个
Option<Box<Any>>
来保存与函数相关的“用户数据”。即便如此,它也不是惯用的Rust。Rust将数据与函数关联起来的方法是在匿名 * 闭包 * 中捕获它,就像在现代C++中一样。由于闭包不是fn
,set_callback
将需要接受其他类型的函数对象。作为泛型函数对象的回调
在Rust和C++中,具有相同调用签名的闭包具有不同的大小,以适应它们可能捕获的不同值。另外,每个闭包定义为闭包的值生成唯一的匿名类型。由于这些约束,结构体不能命名其
callback
字段的类型,也不能使用别名。一种在结构域中嵌入闭包而不引用具体类型的方法是使结构成为 generic。结构将自动调整它的大小和回调的类型,以适应你传递给它的具体函数或闭包:
和前面一样,
set_callback()
将接受用fn
定义的函数,但是这个函数也将接受闭包|| println!("hello world!")
,以及捕获值的闭包,比如|| println!("{}", somevar)
。由set_callback
的调用者提供的闭包将自动从其环境中捕获所需的数据,并在调用时使其可用。但是
FnMut
是怎么回事呢?为什么不只是Fn
呢?因为闭包保存捕获的值,所以在调用闭包时必须应用Rust通常的变异规则。根据闭包如何处理它们保存的值,它们被分为三个家族,每个家族都标记有一个特性:Fn
是只读取数据的闭包,可以安全地多次调用,可能来自多个线程。上面的两个闭包都是Fn
。FnMut
是修改数据的闭包,例如,通过写入捕获的mut
变量。它们也可以被多次调用,但不能并行调用。(从多个线程调用FnMut
闭包将导致数据争用,因此只能在互斥保护下进行。)闭包对象必须由调用方声明为可变的。FnOnce
是一些闭包,它们会 * 消耗 * 它们捕获的一些数据,例如,通过将捕获的值传递给一个函数,该函数通过值来获取它。顾名思义,这些闭包只能被调用一次,并且调用者必须拥有它们。有些违反直觉的是,当指定一个trait绑定到一个接受闭包的对象类型时,
FnOnce
实际上是最宽松的一个。声明一个泛型回调类型必须满足FnOnce
trait意味着它将接受字面上的任何闭包。但这是有代价的:这意味着保持器只允许调用它一次。由于process_events()
可以选择多次调用回调函数,并且方法本身也可以被多次调用,因此下一个最宽松的界限是FnMut
。注意,我们必须将process_events
标记为正在修改self
。非泛型回调:函数特征对象
尽管回调的泛型实现非常高效,但它有严重的接口限制。它要求每个
Processor
示例都用一个具体的回调类型参数化,这意味着单个Processor
只能处理一个回调类型。假定每个闭包都有一个不同的类型,泛型Processor
无法处理后跟proc.set_callback(|| println!("world"))
的proc.set_callback(|| println!("hello"))
。扩展该结构以支持两个回调字段将需要将整个结构参数化为两种类型,如果回调的数量需要是动态的,则添加更多的类型参数将不起作用,例如实现维护不同回调的向量的add_callback
函数。要删除type参数,我们可以利用trait objects,Rust的特性允许基于trait自动创建动态接口,这有时被称为 type erasure,是C中一种流行的技术1(http://davekilian.com/cpp-type-erasure.html) 2(https://akrzemi1.wordpress.com/2013/11/18/type-erasure-part-i/),不要与Java和FP语言对该术语的不同使用相混淆。熟悉C的读者会将实作
Fn
的闭包与Fn
Trait对象之间的区别,视为与C++中一般函式对象与std::function
值之间的区别相同。一个trait对象是通过用
&
操作符借用一个对象,并将其强制转换为一个对特定trait的引用来创建的。(std::unique_ptr
的Rust对等用法),其功能相当于trait对象。如果
Processor
存储Box<dyn FnMut()>
,则它不再需要是泛型的,但是set_callback
* 方法 * 现在通过impl Trait
argument接受泛型c
。set_callback
的泛型参数并不限制处理器接受什么类型的回调,因为接受的回调的类型与存储在Processor
结构中的类型分离。盒装密封件内的参考寿命
set_callback
接受的c
参数类型上的'static
生存期限制是一种简单的方法,可以让编译器相信 references 包含在c
中,它可能是一个引用其环境的闭包,只引用全局值,因此在回调的整个使用过程中保持有效。但是静态限制也是非常苛刻的:虽然它很好地接受拥有对象的闭包(我们在上面通过将闭包设为move
来确保这一点),但它拒绝引用本地环境的闭包,即使它们只引用比处理器更持久的值,而且实际上是安全的。因为我们只需要回调在处理器存在的情况下仍然存在,所以我们应该尝试将它们的生存期与处理器的生存期绑定在一起,这比
'static
的限制要宽松一些。它不再编译。这是因为set_callback
创建了一个新的box并将其赋值给定义为Box<dyn FnMut()>
的callback
字段。由于定义没有为boxed trait对象指定生存期,'static
是隐含的,而且指派会有效地延长存留期(从未命名的任意回呼存留期延长到'static
),这是不允许的。修正的方式是提供处理器的明确存留期,并将该存留期系结到方块中的指涉,以及set_callback
所接收的回呼中的指涉:当这些生存期被显式化后,就不再需要使用
'static
了。闭包现在可以引用本地的s
对象,也就是说,不再需要是move
,只要s
的定义放在p
的定义之前,以确保字符串在处理器之后仍然存在。piztneat2#
如果您愿意处理生存期问题,但又负担不起堆分配,那么下面是一个使用引用来实现回调的实现:
imzjd6km3#
对于使用回调的方案类型,您应该考虑Promise替代方案。它比回调更易于使用,因为它避免了嵌套(callback hell)。
请考虑以下内容:
对于任何计算:
Runner
特征:run()
的代码,该代码将由单独的线程执行最后,这就是“魔术”:
w51jfk4q4#
https://stackoverflow.com/a/70943671/286335的简单版本,仅用于闭包。
其中
do_async
为