gpt4 book ai didi

callback - Rust 中的惯用回调

转载 作者:行者123 更新时间:2023-11-29 07:40:04 34 4
gpt4 key购买 nike

在 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 ?一个例子将是惊人的。

最佳答案

简短回答:为了获得最大的灵活性,您可以将回调存储为盒装 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: callback };
p.process_events();
}
和以前一样,回调的新定义将能够接受用 fn 定义的顶级函数。 ,但这个也将接受闭包 || println!("hello world!") ,以及捕获值的闭包,例如 || println!("{}", somevar) .因此,处理器不需要 userdata陪伴回调; set_callback 的调用者提供的闭包将自动从其环境中捕获所需的数据,并在调用时使其可用。
但是 FnMut 怎么办? ,为什么不只是 Fn ?由于闭包保存了捕获的值,因此在调用闭包时必须应用 Rust 的通常变异规则。根据闭包对它们所持有的值的作用,它们被分为三个系列,每个系列都标有一个特征:
  • Fn是只读取数据的闭包,可以安全地多次调用,可能来自多个线程。以上两个闭包都是Fn .
  • FnMut是修改数据的闭包,例如通过写入捕获的 mut变量。它们也可能被多次调用,但不能并行调用。 (从多个线程调用 FnMut 闭包会导致数据竞争,因此只能通过互斥锁的保护来完成。)闭包对象必须由调用者声明为可变的。
  • FnOnce是消耗它们捕获的一些数据的闭包,例如通过将捕获的值传递给按值获取它的函数。顾名思义,这些只能被调用一次,并且调用者必须拥有它们。

  • 有点违反直觉,当为接受闭包的对象类型指定 trait bound 时, FnOnce实际上是最宽容的。声明通用回调类型必须满足 FnOnce trait 意味着它将接受任何闭包。但这是有代价的:这意味着持有者只能调用一次。自 process_events()可以选择多次调用回调,并且由于方法本身可能被多次调用,下一个最宽松的界限是 FnMut .请注意,我们必须标记 process_events作为变异 self .
    非泛型回调:函数特征对象
    尽管回调的通用实现非常高效,但它有严重的接口(interface)限制。它需要每个 Processor使用具体回调类型参数化的实例,这意味着单个 Processor只能处理单一的回调类型。鉴于每个闭包都有不同的类型,泛型 Processor无法处理 proc.set_callback(|| println!("hello"))其次是 proc.set_callback(|| println!("world")) .扩展结构以支持两个回调字段需要将整个结构参数化为两种类型,随着回调数量的增加,这将很快变得笨拙。如果回调的数量需要是动态的,例如,添加更多类型参数将不起作用。实现 add_callback维护不同回调向量的函数。
    要删除类型参数,我们可以利用 trait objects ,Rust 的特性,允许基于特征自动创建动态接口(interface)。这有时被称为类型删除,是 C++ 中的一种流行技术 [1] [2] ,不要与 Java 和 FP 语言对该术语的一些不同用法混淆。熟悉 C++ 的读者会认识到实现 Fn 的闭包之间的区别。和一个 Fn trait 对象等同于一般函数对象和 std::function 的区别C++ 中的值。
    通过借用 & 的对象来创建 trait 对象。运算符并将其强制转换为对特定特征的引用。在这种情况下,由于 Processor需要拥有回调对象,我们不能使用借用,但必须将回调存储在堆分配中 Box<dyn Trait> (Rust 等效于 std::unique_ptr ),它在功能上等效于特征对象。
    Processor专卖店 Box<dyn FnMut()> ,它不再需要是通用的,而是 set_callback方法现在接受泛型 c通过 impl Trait argument .因此,它可以接受任何类型的可调用对象,包括带状态的闭包,并在将其存储在 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();
    }
    盒装闭包内引用文献的生命周期 'static c 类型的生命周期限制 set_callback 接受的参数是一种说服编译器相信包含在 c 中的引用的简单方法。 ,它可能是一个引用其环境的闭包,仅引用全局值,因此在回调的整个使用过程中将保持有效。但是静态边界也非常严厉:虽然它接受拥有对象的闭包很好(我们在上面通过制作闭包 move 确保了这一点),但它拒绝引用本地环境的闭包,即使它们只引用到比处理器生命周期更长并且实际上是安全的值。
    因为只要处理器还活着,我们就只需要回调活着,所以我们应该尝试将它们的生命周期与处理器的生命周期联系起来,这比 'static 更不严格。 .但是如果我们只是删除 'static生命周期从 set_callback 开始,它不再编译。这是因为 set_callback创建一个新框并将其分配给 callback字段定义为 Box<dyn FnMut()> .由于定义没有为装箱的 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 的定义之前以确保字符串比处理器生命周期更长。

    关于callback - Rust 中的惯用回调,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41081240/

    34 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com