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

Calling a Stack-Allocated Closure Stored in a Struct in Rust

我在这样的结构中存储闭包:

#[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>)?为什么调用闭包会移动它?大概调用它不会改变函数本身。

正在移动闭包,因为 FnOnce::call_once 按值取用 self。此合同强制保证该函数不会被多次调用。

如果你确实最多调用一次闭包,并且你想使用 FnOnce 特性,那么你的结构需要获得该闭包的所有权(并且你需要使你的结构通用在闭合类型上)。请注意,调用闭包会将闭包移出您的结构,从而使整个结构无效;您可以通过将 FnOnce 包装在 Option 中并 take 在调用它之前将闭包从 Option 中取出来解决这个问题。

如果您可能多次调用闭包,您不想获得闭包的所有权,或者您不想让您的结构在闭包类型上通用,那么您应该使用 FnFnMut 代替。 Fn::call takes self by reference and FnMut::call_mut 通过可变引用获取 self。由于两者都接受引用,因此您可以对它们使用特征对象。

正如 Francis 所解释的那样,声明一个闭包 FnOnce 告诉 Rust 你接受最广泛的 class 闭包,包括那些耗尽它们捕获的对象的闭包。编译器通过在调用时销毁闭包对象本身(通过将其移动到它自己的 call 方法中)来确保此类闭包仅被调用一次。

可以使用 FnOnce 并且在闭包上仍然没有 S 泛型,但是它需要一些工作来设置,这样闭包就不可能被调用超过一次:

  • 闭包必须存储在一个Option中,所以它的内容可以是"stolen"并且Option替换为None(这部分确保闭包赢了'被调用两次);
  • 发明一个特征,知道如何从选项中窃取闭包并调用它(或者如果闭包已经被盗则做其他事情);
  • S 中存储对特征对象的引用 - 这使得相同的 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 实现,为每个闭包生成并由 &mut Callable 胖指针的 vtable 引用在 S.

中初始化 func 时创建