Rust中的惯用回调函数

173
在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个回答

361
简短回答:为了最大限度的灵活性,您可以将回调作为一个装箱的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(); // hello world!
}

这段代码可以扩展,包括一个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的定义以确保字符串存活超过处理器。


27
哇,我认为这是我在SO问题中得到的最好的答案!谢谢!解释得很清楚。不过有一点我不明白——在最后一个例子中,为什么CB必须是“静态”的? - Timmmm
11
结构体字段中使用的 Box<FnMut()> 表示 Box<FnMut() + 'static>。大致意思是 "这个被盒装的 trait 对象不包含任何引用,或者它包含的所有引用都比 'static 生命周期更长或相等"。这可以防止回调函数通过引用捕获局部变量。 - bluss
2
@Timmmm 更多关于'static绑定的详细信息,请参阅单独的博客文章 - user4815162342
7
这是一个很棒的回答,感谢@user4815162342提供。 - Dash83
1
如果您想从特定结构中解析成员函数,该怎么办?在Rust中是否可能,或者我们必须使用全局作用域函数和闭包? - DrStrange
显示剩余6条评论

3
如果您希望处理生存期并且无法承担堆分配,那么这里有一个使用引用实现回调的实现:
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
    }
}

1

如果涉及到回调函数的场景,建议考虑使用 Promise 替代。相比于回调函数,Promise 更易于使用,因为它避免了嵌套(回调地狱)。

可以参考以下内容:

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

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

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

对于任何计算:

  • 定义一个结构体,其字段为其输入(名称不重要)。
  • 实现Runner trait:
    • 选择返回什么
    • 编写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;
}

1
一个更简单的闭包版本的https://dev59.com/k1gR5IYBdhLWcg3wouh6#70943671
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;
} // ()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接