Rust中的惯用回调 [英] Idiomatic callbacks in Rust

查看:208
本文介绍了Rust中的惯用回调的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In C/C++ I'd normally do callbacks with a plain function pointer, maybe passing a void* userdata parameter too. Something like this:

typedef void (*Callback)();

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

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

在Rust中这样做的惯用方式是什么?具体来说,我的 setCallback()函数应该采用什么类型, mCallback 应该是什么类型? Fn 应该吗?也许 FnMut ?是否将其保存在盒装中?一个例子将是惊人的。

What is the idiomatic way of doing this in Rust? Specifically, what types should my setCallback() function take, and what type should mCallback be? Should it take an Fn? Maybe FnMut? Do I save it Boxed? An example would be amazing.

推荐答案

简短答案:为了获得最大的灵活性,您可以将回调存储为带框的 FnMut 对象,对回调类型使用通用的回调设置器。答案中的最后一个示例中显示了此代码。有关更详细的解释,请继续阅读。

Short answer: For maximum flexibility, you can store the callback as a boxed FnMut object, with the callback setter generic on callback type. The code for this is shown in the last example in the answer. For a more detailed explanation, read on.

该问题中与C ++代码最接近的等效项是将回调声明为 fn 类型。 fn 封装了由 fn 关键字定义的函数,就像C ++的函数指针一样:

The closest equivalent of the C++ code in the question would be declaring callback as a fn type. fn encapsulates functions defined by the fn keyword, much like C++'s function pointers:

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方法是在匿名 closure 中捕获数据,就像在现代C ++中一样。由于闭包不是 fn ,因此 set_callback 将需要接受其他类型的函数对象。

This code could be extended to include an Option<Box<Any>> to hold the "user data" associated with the function. Even so, it would not be idiomatic Rust. The Rust way to associate data with a function is to capture it in an anonymous closure, just like in modern C++. Since closures are not fn, set_callback will need to accept other kinds of function objects.

在具有相同调用签名的Rust和C ++闭包中,它们的大小不同,以适应它们可能捕获的不同值。此外,每个闭包定义都会为闭包的值生成一个唯一的匿名类型。由于这些限制,该结构无法命名其回调字段的类型,也不能使用别名。

In both Rust and C++ closures with the same call signature come in different sizes to accommodate the different values they might capture. Additionally, each closure definition generates a unique anonymous type for the closure's value. Due to these constraints, the struct cannot name the type of its callback field, nor can it use an alias.

一种方法通过使struct generic 来将闭包嵌入struct字段而不引用具体类型。该结构将自动调整其大小以及您传递给它的具体函数或闭包的回调类型:

One way to embed a closure in the struct field without referring to a concrete type is by making the struct generic. The struct will automatically adapt its size and the type of callback for the concrete function or closure you pass to it:

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 即可进行回调;调用方 set_callback 提供的闭包将自动从其环境中捕获所需的数据,并在调用时使其可用。

As before, the new definition of callback will be able to accept top-level functions defined with fn, but this one will also accept closures as || println!("hello world!"), as well as closures that capture values, such as || println!("{}", somevar). Because of this the processor doesn't need userdata to accompany the callback; the closure provided by the caller of set_callback will automatically capture the data it needs from its environment and have it available when invoked.

但是 FnMut 怎么了,为什么不只是 Fn ?由于闭包保留捕获的值,因此在调用闭包时,Rust的常规突变规则必须适用。根据闭包对它们持有的值的处理方式,将它们分为三个家族,每个家族都标有一个特征:

But what's the deal with the FnMut, why not just Fn? Since closures hold captured values, Rust's usual mutation rules must apply when calling the closure. Depending on what the closures do with the values they hold, they are grouped in three families, each marked with a trait:


  • Fn 是仅读取数据的闭包,可以安全地多次调用(可能来自多个线程)。上面两个关闭都是 Fn

  • FnMut 是用于修改数据的关闭,例如通过写入捕获的 mut 变量。它们也可能被多次调用,但不能并行调用。 (从多个线程调用 FnMut 闭包将导致数据争用,因此只能在保护互斥锁的情况下完成。)闭包对象必须声明为可变的

  • FnOnce 是闭包,它们使用一些捕获的数据,例如通过将捕获的值移动到具有其所有权的函数。顾名思义,它们只能被调用一次,并且调用者必须拥有它们。

  • Fn are closures that only read data, and may be safely called multiple times, possibly from multiple threads. Both above closures are Fn.
  • FnMut are closures that modify data, e.g. by writing to a captured mut variable. They may also be called multiple times, but not in parallel. (Calling a FnMut closure from multiple threads would lead to a data race, so it can only be done with the protection of a mutex.) The closure object must be declared mutable by the caller.
  • FnOnce are closures that consume some od the data they capture, e.g. by moving a captured value to a function that takes its ownership. As the name implies, these may be called only once, and the caller must own them.

在指定绑定到该特性的特征时,有些反直觉接受闭包的对象类型, FnOnce 实际上是最宽松的对象。声明通用回调类型必须满足 FnOnce 特征意味着这将接受字面上的任何闭包。但这带有价格:这意味着持有人只能叫一次。由于 process_events()可能会选择多次调用回调,并且由于方法本身可能被多次调用,因此下一个允许的范围是 FnMut 。请注意,我们必须将 process_events 标记为变异 self

Somewhat counter-intuitively, when specifying a trait bound for the type of an object that accepts a closure, FnOnce is actually the most permissive one. Declaring that a generic callback type must satisfy the FnOnce trait means that it will accept literally any closure. But that comes with a price: it means the holder is only allowed to call it once. Since process_events() may opt to invoke the callback multiple times, and as the method itself may be called more than once, the next most permissive bound is FnMut. Note that we had to mark process_events as mutating self.

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

Even though the generic implementation of the callback is extremely efficient, it has serious interface limitations. It requires each Processor instance to be parameterized with a concrete callback type, which means that a single Processor can only deal with a single callback type. Given that each closure has a distinct type, the generic Processor cannot handle proc.set_callback(|| println!("hello")) followed by proc.set_callback(|| println!("world")). Extending the struct to support two callbacks fields would require the whole struct to be parameterized to two types, which would quickly become unwieldy as the number of callbacks grows. Adding more type parameters wouldn't work if the number of callbacks needed to be dynamic, e.g. to implement an add_callback function that maintains a vector of different callbacks.

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

To remove the type parameter, we can take advantage of trait objects, the feature of Rust that allows automatic creation of dynamic interfaces based on traits. This is sometimes referred to as type erasure and is a popular technique in C++[1][2], not to be confused with Java and FP languages' somewhat different use of the term. Readers familiar with C++ will recognize the distinction between a closure that implements Fn and an Fn trait object as equivalent to the distinction between general function objects and std::function values in C++.

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

A trait object is created by borrowing an object with the & operator and casting or coercing it to a reference to the specific trait. In this case, since Processor needs to own the callback object, we cannot use borrowing, but must store the callback in a heap-allocated Box<dyn Trait> (the Rust equivalent of std::unique_ptr), which is functionally equivalent to a trait object.

如果处理器存储 Box< dyn FnMut()> ,则它不再是通用的,而是 set_callback 方法现在可以通过c : //doc.rust-lang.org/edition-guide/rust-2018/trait-system/impl-trait-for-returning-complex-types-with-ease.html rel = noreferrer> impl特性参数。这样,它可以接受任何类型的可调用对象,包括带状态的闭包,并在将其存储在 Processor 中之前将其正确装箱。 set_callback 的通用参数不限制处理器接受哪种回调,因为接受的回调的类型与处理器结构。

If Processor stores Box<dyn FnMut()>, it no longer needs to be generic, but the set_callback method now accepts a generic c via an impl Trait argument. As such, it can accept any kind of callable, including closures with state, and properly box it before storing it in the Processor. The generic argument to set_callback doesn't limit what kind of callback the processor accepts, as the type of the accepted callback is decoupled from the type stored in the Processor struct.

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


盒装闭包内部引用的生命周期


'static 生命周期受 set_callback c 参数类型的限制>是使编译器确信 c 中包含的 references 的一种简单方法,这可能是引用其环境的闭包,仅引用global值,因此将在整个回调使用期间保持有效。但是静态绑定也是非常笨拙的:虽然它接受拥有对象的闭包就很好(我们在上面通过使闭包 move 来确保),但它拒绝闭包引用本地环境,即使它们仅引用超出处理器的生存期并且实际上是安全的值。

Lifetime of references inside boxed closures

The 'static lifetime bound on the type of the c argument accepted by set_callback is a simple way to convince the compiler that references contained in c, which might be a closure that refers to its environment, only refer to global values and will therefore remain valid throughout the use of the callback. But the static bound is also very heavy-handed: while it accepts closures that own objects just fine (which we've ensured above by making the closure move), it rejects closures that refer to local environment, even when they only refer to values that outlive the processor and would in fact be safe.

由于只要处理器处于活动状态,我们只需要保持回调即可,我们应该尝试将它们的生命周期与处理器的生命周期联系起来,这比'static 严格。但是,如果我们只从 set_callback 中删除​​静态生存期绑定,它将不再编译。这是因为 set_callback 创建一个新框,并将其分配给定义为 Box<的 callback 字段; dyn FnMut()> 。由于定义未为装箱的特征对象指定生存期,因此暗含了'static ,并且赋值将有效地延长生存期(从回调的未命名的任意生存期开始)改为'static ),这是不允许的。解决方法是为处理器提供明确的生存期,并将该生存期与框中的引用和 set_callback

As we only need the callbacks alive as long as the processor is alive, we should try to tie their lifetime to that of the processor, which is a less strict bound than 'static. But if we just remove the 'static lifetime bound from set_callback, it no longer compiles. This is because set_callback creates a new box and assigns it to the callback field defined as Box<dyn FnMut()>. Since the definition doesn't specify a lifetime for the boxed trait object, 'static is implied, and the assignment would effectively widen the lifetime (from an unnamed arbitrary lifetime of the callback to 'static), which is disallowed. The fix is to provide an explicit lifetime for the processor and tie that lifetime to both the references in the box and the references in the callback received by 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 的定义之前,以确保该字符串的寿命超过处理器。

With these lifetimes being made explicit, it is no longer necessary to use 'static. The closure can now refer to the local s object, i.e. no longer has to be move, provided that the definition of s is placed before the definition of p to ensure that the string outlives the processor.

这篇关于Rust中的惯用回调的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆