Rust中的惯用回调

时间:2016-12-10 23:02:51

标签: 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;
};

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

2 个答案:

答案 0 :(得分:121)

简短回答:为了获得最大的灵活性,您可以将回调存储为盒装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 mut p = Processor { callback: simple_callback };
    p.process_events();         // hello world!
}

此代码可以扩展为包含Option<Box<Any>>以保存&#34;用户数据&#34;与功能相关联。即便如此,它也不会是惯用的Rust。将数据与函数关联的Rust方法是在匿名闭包中捕获它,就像在现代C ++中一样。由于闭包不是fnset_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();
}

和以前一样,回调的新定义将能够接受用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函数来维护不同回调的向量。

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

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

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

答案 1 :(得分:0)

如果你愿意处理生命周期并且负担不起堆分配,那么这里有一个使用引用来实现回调的实现:

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