Rust 中的惯用回调

在 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?举个例子就好了。

43606 次浏览

简短的回答: 为了获得最大的灵活性,您可以将回调存储为一个装箱的 FnMut对象,并在回调类型上使用回调 setter 泛型。这方面的代码显示在答案的最后一个示例中。欲了解更详细的解释,请继续阅读。

“函数指针”: 作为 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(); // hello world!
}

这段代码可以扩展为包含一个 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 };
p.process_events();
}

和以前一样,set_callback()将接受用 fn定义的函数,但是这个函数也将接受作为 || println!("hello world!")的闭包,以及捕获值的闭包,例如 || println!("{}", somevar)。因此,处理器不需要 userdata来伴随回调; set_callback的调用方提供的闭包将自动从其环境中捕获它所需的数据,并在调用时使其可用。

但是 FnMut是怎么回事,为什么不只是 Fn呢?由于闭包包含捕获的值,因此在调用闭包时必须应用 Rust 通常的变异规则。根据闭包对它们所持有的值的处理方式,它们被分成三个系列,每个系列都标有一个 trait:

  • Fn是只读取数据的闭包,可以安全地多次调用,可能来自多个线程。上述两个闭包都是 Fn
  • FnMut是修改数据的闭包,例如通过写入捕获的 mut变量。它们也可以被多次调用,但不能并行调用。(从多个线程调用 FnMut闭包将导致数据竞争,因此只能在互斥锁的保护下进行。)调用方必须声明闭包对象是可变的。
  • FnOnce是一些闭包,消耗捕获它们捕获的一些数据,例如,通过将捕获的值传递给一个函数,该函数根据值获取该值。顾名思义,这些只能调用一次,调用者必须拥有它们。

在为接受闭包的对象类型指定约束的 trait 时,FnOnce实际上是最宽松的,这有点违反直觉。声明泛型回调类型必须满足 FnOnce trait 意味着它将接受任何闭包。但这是有代价的: 这意味着持有者只能喊一次。由于 process_events()可能选择多次调用回调,而且由于方法本身可能被调用不止一次,因此下一个最宽松的界限是 FnMut。注意,我们必须将 process_events标记为突变的 self

非泛型回调: 函数 trait 对象

尽管回调的通用实现非常高效,但它有严重的接口限制。它要求使用具体的回调类型参数化每个 Processor实例,这意味着单个 Processor只能处理单个回调类型。鉴于每个闭包都有一个不同的类型,通用 Processor不能处理 proc.set_callback(|| println!("hello"))proc.set_callback(|| println!("world"))。扩展 struct 以支持两个回调字段将需要将整个 struct 参数化为两种类型,随着回调数量的增加,这将很快变得难以处理。如果回调的数量需要是动态的,那么添加更多的类型参数将不起作用,例如,实现一个 add_callback函数来维护不同回调的向量。

为了删除类型参数,我们可以利用 特征对象,Rust 的特性,它允许基于 trait 自动创建动态接口。这有时被称为 类型删除,是 C + + [1][2]中的一种流行技术,不要与 Java 和 FP 语言对这个术语的不同使用混淆。熟悉 C + + 的读者会认识到实现 Fn的闭包和 Fn trait 对象之间的区别,这与 C + + 中一般函数对象和 std::function值之间的区别是等价的。

特征对象是通过与 &操作符借用一个对象并将其强制转换为对特定特征的引用而创建的。在这种情况下,由于 Processor需要拥有回调对象,因此我们不能使用借用,而是必须将回调存储在堆分配的 Box<dyn Trait>(相当于 std::unique_ptr的 Rust)中,该 Box<dyn Trait>在功能上等效于一个 trait 对象。

如果 Processor存储 Box<dyn FnMut()>,它不再需要是通用的,但是 set_callback 方法现在通过 impl Trait参数接受一个通用的 c。因此,它可以接受任何类型的可调用,包括带状态的闭包,并在将其存储在 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();
}

已装箱闭包内引用的生存期

绑定在 set_callback接受的 c参数类型上的 'static生命周期是一种简单的方法,可以让编译器相信 c中包含的 参考文献(可能是一个指向其环境的闭包)只引用全局值,因此在整个回调使用过程中都是有效的。但是静态界限也是非常粗暴的: 虽然它接受拥有对象的闭包(我们在上面通过使用闭包 move确保了这一点) ,但是它拒绝引用本地环境的闭包,即使它们只引用比处理器存在时间长且实际上是安全的值。

因为只要处理器还活着,我们就需要活着的回调,所以我们应该尝试将它们的生命周期与处理器的生命周期绑定在一起,这比 'static的限制要宽松一些。但是,如果我们只是从 set_callback中移除 'static生存期绑定,它就不再进行编译。这是因为 set_callback创建了一个新框,并将其分配给定义为 Box<dyn FnMut()>callback字段。由于定义没有为装箱的 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的定义之前,以确保字符串比处理器存在的时间长。

如果您愿意处理生命周期,但是无法负担堆分配,那么这里有一个使用引用来实现回调的实现:

use core::ffi::c_void;
use core::mem::transmute;
use core::ptr::null_mut;
use core::marker::PhantomData;


/// ErasedFnPointer can either points to a free function or associated one that
/// `&mut self`
struct ErasedFnPointer<'a, T, Ret> {
struct_pointer: *mut c_void,
fp: *const (),
// The `phantom_*` field is used so that the compiler won't complain about
// unused generic parameter.
phantom_sp: PhantomData<&'a ()>,
phantom_fp: PhantomData<fn(T) -> Ret>,
}


impl<'a, T, Ret> Copy for ErasedFnPointer<'a, T, Ret> {}
impl<'a, T, Ret> Clone for ErasedFnPointer<'a, T, Ret> {
fn clone(&self) -> Self {
*self
}
}


impl<'a, T, Ret> ErasedFnPointer<'a, T, Ret> {
pub fn from_associated<S>(struct_pointer: &'a mut S, fp: fn(&mut S, T) -> Ret)
-> ErasedFnPointer<'a, T, Ret>
{
ErasedFnPointer {
struct_pointer: struct_pointer as *mut _ as *mut c_void,
fp: fp as *const (),
phantom_sp: PhantomData,
phantom_fp: PhantomData,
}
}
    

pub fn from_free(fp: fn(T) -> Ret) -> ErasedFnPointer<'static, T, Ret> {
ErasedFnPointer {
struct_pointer: null_mut(),
fp: fp as *const (),
phantom_sp: PhantomData,
phantom_fp: PhantomData,
}
}
    

pub fn call(&self, param: T) -> Ret {
if self.struct_pointer.is_null() {
let fp = unsafe { transmute::<_, fn(T) -> Ret>(self.fp) };
fp(param)
} else {
let fp = unsafe { transmute::<_, fn(*mut c_void, T) -> Ret>(self.fp) };
fp(self.struct_pointer, param)
}
}
}


fn main() {
let erased_ptr = ErasedFnPointer::from_free(|x| {
println!("Hello, {}", x);
x
});
erased_ptr.call(2333);
    

println!("size_of_val(erased_ptr) = {}", core::mem::size_of_val(&erased_ptr));


ErasedFnPointer::from_associated(
&mut Test { x: 1},
Test::f
).call(1);
    

let mut x = None;
ErasedFnPointer::from_associated(&mut x, |x, param| {
*x = Some(param);
println!("{:#?}", x);
}).call(1);
}


struct Test {
x: i32
}
impl Test {
fn f(&mut self, y: i32) -> i32 {
let z = self.x + y;
println!("Hello from Test, {}", z);
z
}
}

对于使用回调的场景类型,您应该考虑使用“承诺”替代方案。 它比回调更容易使用,因为它避免了嵌套(回复地狱)。

想想这个:

fn main() {
let fut = do_async(&Calculation{ value: 12 });


let resp = fut().unwrap(); // call fut() to wait for the respbnse


println!("{}", resp);
}

任何计算:

  • 定义一个字段为其输入的结构(名称不重要)。
  • 你实现了 Runner特性:
    • 你选择归还什么
    • 编写 run()的代码,它将由一个单独的线程执行
struct Calculation {  // <---- choose: name
value: i32  // <----- choose: inputs for your async work
}


impl Runner for Calculation {
type ReturnType = i32;  // <--- choose: calculation return type


fn run(&self) -> Option<Self::ReturnType> {  // <-- implement: code executed by a thread
println!("async calculation starts");
thread::sleep(Duration::from_millis(3000));


return Some(self.value * 2);
}
}

最后,这就是“魔法”:

trait Runner: Send + Sync {
type ReturnType: Send; // associated type


fn run(&self) -> Option<Self::ReturnType>;
}


fn do_async<TIn: Runner>(f: &'static TIn) -> impl FnOnce()-> Option<TIn::ReturnType> {
let (sender, receiver) = channel::<Option<TIn::ReturnType>>();


let hand = thread::spawn(move || {
sender.send(f.run()).unwrap();
});


let f = move || -> Option<TIn::ReturnType> {
let res = receiver.recv().unwrap();
hand.join().unwrap();
return res;
};


return f;
}

https://stackoverflow.com/a/70943671/286335的一个简单版本,仅用于闭包。

fn main() {
let n = 2;


let fut = do_async(move || {
thread::sleep(Duration::from_millis(3000));
return n * 1234;
});


let resp = fut(); // call fut() to wait for the response


println!("{}", resp);
} // ()

do_async在哪里

fn do_async<TOut, TFun>(foo: TFun) -> (impl FnOnce() -> TOut)
where
TOut: Send + Sync + 'static,
TFun: FnOnce() -> TOut + Send + Sync + 'static,
{
let (sender, receiver) = channel::<TOut>();


let hand = thread::spawn(move || {
sender.send(foo()).unwrap();
});


let f = move || -> TOut {
let res = receiver.recv().unwrap();
hand.join().unwrap();
return res;
};


return f;
} // ()