在Rust中调用存储在结构体中的堆栈分配闭包

4

我正在像这样将闭包存储在结构体中:

#[derive(Clone)]
struct S<'a> {
    func: &'a FnOnce() -> u32
}

fn main() {
    let s = S { func: &|| 0 };
    let val = (s.func)();
    println!("{}", val);
}

当我编译时,s.func 不能被移动以执行自身。我理解为什么它不能被移动(即它只是一个引用,其大小在编译时未知),但不知道为什么它被移动了 - 是因为通过特性实现了闭包吗?
以下是错误消息:
error[E0161]: cannot move a value of type std::ops::FnOnce() -> u32:
the size of std::ops::FnOnce() -> u32 cannot be statically determined
 --> main.rs:8:15
  |
8 |     let val = (s.func)();
  |               ^^^^^^^^

error[E0507]: cannot move out of borrowed content
 --> main.rs:8:15
  |
8 |     let val = (s.func)();
  |               ^^^^^^^^ cannot move out of borrowed content

error: aborting due to 2 previous errors

这是唯一的解决方法吗?将闭包存储在堆上(通过Box<FnOnce() -> u32>)?为什么调用闭包会移动它?推测调用闭包并不会改变函数本身。

2个回答

6
闭包被移动的原因是因为 FnOnce::call_once 通过值获取了self。这个约定保证函数不会被调用多次。
如果你确实只会调用闭包一次,并且想要使用 FnOnce trait,那么你的结构体需要拥有该闭包(你需要将结构体泛型化到该闭包类型上)。请注意,调用闭包会将其移出结构体,从而使整个结构体失效;您可以通过在 Option 中包装 FnOnce 并在调用之前 take 出闭包来解决这个问题。
如果你可能会多次调用闭包,不想拥有该闭包,或者不想让你的结构体泛型化到该闭包类型上,则应使用 FnFnMutFn::call 通过引用获取 self,而FnMut::call_mut 通过可变引用获取 self。由于两者都接受引用,因此您可以使用 trait 对象。

这非常有帮助,谢谢。我最终使用了Fn而不是FnOnce,因为我想创建一个结构体的const实例,但整个回复对于理解整个问题都非常有帮助。 - cderwin

2
正如Francis所解释的那样,声明一个闭包FnOnce告诉Rust你接受最广泛的闭包类,包括那些用尽它们捕获的对象的闭包。编译器通过在调用时销毁闭包对象本身(通过将其移动到自己的call方法中)来确保这些闭包仅被调用一次。
可以使用FnOnce而不必在闭包上使用S泛型,但需要一些工作来设置事情,以便闭包不能被可能调用多次:
  • 闭包必须存储在Option中,因此可以“窃取”其内容,并用None替换Option(这部分确保闭包不会被调用两次);
  • 发明一个trait,知道如何从选项中窃取闭包并调用它(或者如果已经窃取了闭包,则执行其他操作);
  • S中存储对trait对象的引用-这使得相同的S类型适用于不同的闭包,而不需要在闭包类型上进行泛型。
结果如下:
trait Callable {
    fn call_once_safe(&mut self, default: u32) -> u32;
}

impl<F: FnOnce() -> u32> Callable for Option<F> {
    fn call_once_safe(&mut self, default: u32) -> u32 {
        if let Some(func) = self.take() {
            func()
        } else {
            default
        }
    }
}

struct S<'a> {
    func: &'a mut Callable
}

impl<'a> S<'a> {
    pub fn invoke(&mut self) -> u32 {
        self.func.call_once_safe(1)
    }
}

fn main() {
    let mut s = S { func: &mut Some(|| 0) };
    let val1 = s.invoke();
    let val2 = s.invoke();
    println!("{} {}", val1, val2);
}

唯一知道闭包细节的地方是为每个闭包生成的特定Option<F>Callable实现,由在初始化S中的func时创建的&mut Callable胖指针的虚函数表引用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接