Rust中的惯用回调

问题描述 投票:57回答:1

在C / C ++中,我通常使用普通函数指针进行回调,也可以传递void* userdata参数。像这样的东西:

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};

在Rust中这样做的惯用方法是什么?具体来说,我的setCallback()功能应该采取什么类型,mCallback应该是什么类型?它应该采取Fn?也许FnMut?我保存它Boxed?一个例子是惊人的。

callback rust
1个回答
127
投票

简短回答:为了获得最大的灵活性,您可以将回调存储为盒装的FnMut对象,回调设置器在回调类型上是通用的。这个代码显示在答案的最后一个例子中。有关更详细的说明,请继续阅读。

"Function pointers": callbacks as 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 mut p = Processor { callback: simple_callback };
    p.process_events();         // hello world!
}

此代码可以扩展为包含Option<Box<Any>>以保存与该函数关联的“用户数据”。即便如此,它也不会是惯用的Rust。将数据与函数关联的Rust方法是在匿名闭包中捕获它,就像在现代C ++中一样。由于闭包不是fnset_callback将需要接受其他类型的函数对象。

Callbacks as generic function objects

在Rust和C ++中,具有相同调用签名的闭包具有不同的大小,以适应它们存储在闭包对象中的不同大小的捕获值。此外,每个闭包站点都会生成一个不同的匿名类型,它是编译时闭包对象的类型。由于这些约束,结构不能按名称或类型别名引用回调类型。

在不引用具体类型的情况下在结构中拥有闭包的一种方法是使结构具有通用性。结构将自动调整其大小和回调的类型,以便传递给它的具体函数或闭包:

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();
}

和以前一样,回调的新定义将能够接受用fn定义的顶级函数,但是这个函数也会接受闭包作为|| println!("hello world!"),以及捕获值的闭包,例如|| println!("{}", somevar)。因此,闭包不需要单独的userdata论证;它可以简单地从其环境中捕获数据,并在调用时可用。

但与FnMut的交易是什么,为什么不只是Fn?由于闭包持有捕获的值,因此Rust会对其强制执行与其他容器对象相同的规则。根据闭包对它们所持有的值的作用,它们分为三个系列,每个系列都标有一个特征:

  • Fn是仅读取数据的闭包,可以安全地多次调用,可能来自多个线程。以上两个封闭都是Fn
  • FnMut是修改数据的闭包,例如:通过写入捕获的mut变量。它们也可能被多次调用,但不能并行调用。 (从多个线程调用FnMut闭包将导致数据争用,因此只能通过保护互斥锁来完成。)闭包对象必须由调用者声明为可变。
  • FnOnce是使用它们捕获的数据的闭包,例如通过将其移动到拥有它们的函数。顾名思义,这些只能调用一次,调用者必须拥有它们。

有点违反直觉,当指定一个接受闭包的对象类型的特征时,FnOnce实际上是最宽松的。声明泛型回调类型必须满足FnOnce特征意味着它将接受任何闭包。但这需要付出代价:这意味着持有人只能拨打一次电话。由于process_events()可能会选择多次调用回调,并且由于方法本身可能不止一次被调用,因此下一个最宽松的界限是FnMut。请注意,我们必须将process_events标记为变异self

Non-generic callbacks: function trait objects

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

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

通过使用&运算符借用对象并将其强制转换或强制转换为对特定特征的引用来创建特征对象。在这种情况下,由于Processor需要拥有回调对象,我们不能使用借用,但必须将回调存储在堆分配的Box<Trait>(相当于std::unique_ptr的Rust)中,这在功能上等同于特征对象。

如果Processor存储Box<FnMut()>,它不再需要是通用的,但set_callback方法现在是通用的,所以它可以正确地包装你给它的任何可调用,然后将盒子存储在Processor中。回调可以是任何类型,只要它不消耗捕获的值。 set_callback是通用的,不会产生上面讨论的限制,因为它不会影响存储在结构中的数据的接口。

struct Processor {
    callback: Box<FnMut()>,
}

impl Processor {
    fn set_callback<CB: 'static + FnMut()>(&mut self, c: CB) {
        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();
}
© www.soinside.com 2019 - 2024. All rights reserved.