Rust中的惯用回调

niwlg2el  于 2022-11-12  发布在  其他
关注(0)|答案(4)|浏览(146)

在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吗?一个例子会很棒。

hpcdzsge

hpcdzsge1#

简短回答:为了获得最大的灵活性,您可以将回调存储为一个装箱的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++中一样。由于闭包不是fnset_callback将需要接受其他类型的函数对象。

作为泛型函数对象的回调

在Rust和C++中,具有相同调用签名的闭包具有不同的大小,以适应它们可能捕获的不同值。另外,每个闭包定义为闭包的值生成唯一的匿名类型。由于这些约束,结构体不能命名其callback字段的类型,也不能使用别名。
一种在结构域中嵌入闭包而不引用具体类型的方法是使结构成为 generic。结构将自动调整它的大小和回调的类型,以适应你传递给它的具体函数或闭包:

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

和前面一样,set_callback()将接受用fn定义的函数,但是这个函数也将接受闭包|| println!("hello world!"),以及捕获值的闭包,比如|| println!("{}", somevar)。由set_callback的调用者提供的闭包将自动从其环境中捕获所需的数据,并在调用时使其可用。
但是FnMut是怎么回事呢?为什么不只是Fn呢?因为闭包保存捕获的值,所以在调用闭包时必须应用Rust通常的变异规则。根据闭包如何处理它们保存的值,它们被分为三个家族,每个家族都标记有一个特性:

  • Fn是只读取数据的闭包,可以安全地多次调用,可能来自多个线程。上面的两个闭包都是Fn
  • FnMut是修改数据的闭包,例如,通过写入捕获的mut变量。它们也可以被多次调用,但不能并行调用。(从多个线程调用FnMut闭包将导致数据争用,因此只能在互斥保护下进行。)闭包对象必须由调用方声明为可变的。
  • FnOnce是一些闭包,它们会 * 消耗 * 它们捕获的一些数据,例如,通过将捕获的值传递给一个函数,该函数通过值来获取它。顾名思义,这些闭包只能被调用一次,并且调用者必须拥有它们。

有些违反直觉的是,当指定一个trait绑定到一个接受闭包的对象类型时,FnOnce实际上是最宽松的一个。声明一个泛型回调类型必须满足FnOnce trait意味着它将接受字面上的任何闭包。但这是有代价的:这意味着保持器只允许调用它一次。由于process_events()可以选择多次调用回调函数,并且方法本身也可以被多次调用,因此下一个最宽松的界限是FnMut。注意,我们必须将process_events标记为正在修改self

非泛型回调:函数特征对象

尽管回调的泛型实现非常高效,但它有严重的接口限制。它要求每个Processor示例都用一个具体的回调类型参数化,这意味着单个Processor只能处理一个回调类型。假定每个闭包都有一个不同的类型,泛型Processor无法处理后跟proc.set_callback(|| println!("world"))proc.set_callback(|| println!("hello"))。扩展该结构以支持两个回调字段将需要将整个结构参数化为两种类型,如果回调的数量需要是动态的,则添加更多的类型参数将不起作用,例如实现维护不同回调的向量的add_callback函数。
要删除type参数,我们可以利用trait objects,Rust的特性允许基于trait自动创建动态接口,这有时被称为 type erasure,是C中一种流行的技术1(http://davekilian.com/cpp-type-erasure.html) 2(https://akrzemi1.wordpress.com/2013/11/18/type-erasure-part-i/),不要与Java和FP语言对该术语的不同使用相混淆。熟悉C的读者会将实作Fn的闭包与Fn Trait对象之间的区别,视为与C++中一般函式对象与std::function值之间的区别相同。

一个trait对象是通过用&操作符借用一个对象,并将其强制转换为一个对特定trait的引用来创建的。(std::unique_ptr的Rust对等用法),其功能相当于trait对象。
如果Processor存储Box<dyn FnMut()>,则它不再需要是泛型的,但是set_callback * 方法 * 现在通过impl Trait argument接受泛型cset_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生存期限制是一种简单的方法,可以让编译器相信 references 包含在c中,它可能是一个引用其环境的闭包,只引用全局值,因此在回调的整个使用过程中保持有效。但是静态限制也是非常苛刻的:虽然它很好地接受拥有对象的闭包(我们在上面通过将闭包设为move来确保这一点),但它拒绝引用本地环境的闭包,即使它们只引用比处理器更持久的值,而且实际上是安全的。
因为我们只需要回调在处理器存在的情况下仍然存在,所以我们应该尝试将它们的生存期与处理器的生存期绑定在一起,这比'static的限制要宽松一些。它不再编译。这是因为set_callback创建了一个新的box并将其赋值给定义为Box<dyn FnMut()>callback字段。由于定义没有为boxed 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的定义之前,以确保字符串在处理器之后仍然存在。

piztneat

piztneat2#

如果您愿意处理生存期问题,但又负担不起堆分配,那么下面是一个使用引用来实现回调的实现:

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
    }
}
imzjd6km

imzjd6km3#

对于使用回调的方案类型,您应该考虑Promise替代方案。它比回调更易于使用,因为它避免了嵌套(callback hell)。
请考虑以下内容:

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

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

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

对于任何计算:

  • 您定义了一个结构体,它的字段是它的输入(名称并不重要)。
  • 您实现了Runner特征:
  • 你选择要返回什么
  • 编写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;
}
w51jfk4q

w51jfk4q4#

https://stackoverflow.com/a/70943671/286335的简单版本,仅用于闭包。

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

相关问题