Rust中的惯用回调

Tim*_*mmm 71 callback rust

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

typedef void (*Callback)();

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

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};
Run Code Online (Sandbox Code Playgroud)

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

use*_*342 136

简短回答:为了获得最大的灵活性,您可以将回调存储为盒装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!
}
Run Code Online (Sandbox Code Playgroud)

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

回调作为通用函数对象

在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();
}
Run Code Online (Sandbox Code Playgroud)

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

但与此有什么关系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的特性允许基于特征自动创建动态接口.这有时被称为类型擦除,并且是C++ [1] [2]中的一种流行技术,不要与Java和FP语言对该术语的某种不同用法相混淆.熟悉C++的读者会认识到实现的闭包FnFn特征对象之间的区别等同于一般函数对象和std::functionC++中的值之间的区别.

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

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

struct Processor {
    callback: Box<dyn 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();
}
Run Code Online (Sandbox Code Playgroud)

  • 哇,我认为这是我遇到的最好的答案!谢谢!完美解释.我不知道的一件小事 - 为什么`CB`在最后的例子中必须是'静态'? (11认同)
  • struct字段中使用的`Box <FnMut()>`表示`Box <FnMut()+'static>`.粗略地说"盒装特征对象不包含任何引用/它包含的任何引用(或等于)`'静态`".它可以防止回调通过引用捕获本地. (8认同)
  • 这是一个绝妙的答案,感谢您提供@user4815162342。 (5认同)
  • @Timmmm很好的问题,你真的关注每一条线.:)事实证明,绑定的`CB:FnMut()`非常通用,因为`FnMut`可以捕获所有类型的值,包括引用."静态"生命周期限制了这些引用(如果有的话)是静态的,这证明它们会比盒子更长.一个更通用的解决方案是使用生命周期''a`对结构进行参数化,并将其用作绑定,但它会不必要地使示例复杂化,除非您在回调中实际使用引用,否则它是不必要的. (3认同)
  • @Timmmm 有关“静态”绑定的更多详细信息,请参阅[单独的博客文章](https://morestina.net/blog/793/closure-lifetimes-in-rust)。 (2认同)