所有权、关闭、FnOnce:很多混乱

Ownership, closures, FnOnce: much confusion

我有以下代码片段:

fn f<T: FnOnce() -> u32>(c: T) {
    println!("Hello {}", c());
}

fn main() {
    let mut x = 32;
    let g  = move || {
        x = 33;
        x
    };

    g(); // Error: cannot borrow as mutable. Doubt 1
    f(g); // Instead, this would work. Doubt 2
    println!("{}", x); // 32
}

疑问1

我什至不能运行关闭一次。

疑问2

... 但我可以根据需要多次调用该闭包,前提是我通过 f 调用它。有趣的是,如果我声明它 FnMut,我会得到与怀疑 1.

相同的错误

疑问3

selfFnFnMutFnOnce特征定义中指的是什么?那是闭包本身吗?还是环境? 例如。来自文档:

pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

您在这里处理两种不同类型的闭包 – FnOnceFnMut。两种类型的闭包都有不同的调用约定。

如果您将闭包定义为

let mut x = 32;
let g  = move || {
    x = 33;
    x
};

编译器会将闭包类型推断为 FnMut。虽然闭包 returns 拥有变量 x,但它仍然可以被多次调用,因为 xCopy,所以编译器选择 FnMut 作为最一般适用类型。

调用 FnMut 闭包时,闭包本身通过可变引用传递。这解释了你的第一个问题——直接调用 g 是行不通的,除非你让它可变,否则你不能对它进行可变引用。我在这里也隐含地回答了你的第三个问题——Fn traits 的调用方法中的 self 指的是闭包本身,它可以被认为是一个包含所有捕获变量的结构。

调用 f(g) 时,将 FnMut 闭包 g 作为 FnOnce 闭包传递给 f()。这是允许的,因为所有 FnOnce 都是 FnMut 的超特征,所以每个实现 FnMut 的闭包也实现了 FnOnce。现在闭包已经转换为FnOnce,也按照FnOnce调用约定调用:

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
在这种情况下,

闭包由 value 传入,因此调用 consumes 闭包。你可以放弃你拥有的任何价值的所有权——它不需要可变就可以工作。

之所以通过f()调用g可以多次调用,是因为gCopy。它只捕获一个整数,因此可以根据需要复制任意多次。每次调用 f() 都会创建一个 g 的新副本,当它在 f().

内部调用时会被消耗掉

需要一些关于 Fn* trait family 的基础知识来理解闭包的实际工作原理。你有以下特点:

  • FnOnce,顾名思义,只能运行一次。如果我们查看文档页面,我们会发现特征定义与您在问题中指定的几乎相同。但最重要的是以下内容:"call" 函数采用 self,这意味着它使用实现 FnOnce 的对象,因此就像任何采用 self 的特征函数一样] 作为参数,它获取对象的所有权。
  • FnMut,允许对捕获的变量进行变异,或者说需要&mut self。这意味着,当你创建一个 move || {} 闭包时,它会将你引用的任何在闭包范围之外的变量移动到闭包的对象中。闭包的对象有一个不可命名的类型,这意味着它对每个闭包都是唯一的。这确实迫使用户采用某种可变版本的闭包,因此 &mut impl FnMut() -> ()mut x: impl FnMut() -> ()
  • Fn,一般认为是最灵活的。这允许用户采用实现特征的对象的不可变版本。此特征的 "call" 函数的函数签名是三个函数中最容易理解的,因为它只需要对闭包的引用,这意味着您在传递或调用它时无需担心所有权。

解决您个人的疑惑:

  • 疑问 1:如上所示,当您 move 将某些内容放入闭包时,该变量现在由闭包拥有。本质上,编译器生成的内容类似于以下伪代码:
struct g_Impl {
    x: usize
}
impl FnOnce() -> usize for g_Impl {
    fn call_once(mut self) -> usize {

    }
}
impl FnMut() -> usize for g_Impl {
    fn call_mut(&mut self) -> usize {
        //Here starts your actual code:
        self.x = 33;
        self.x
    }
}
//No impl Fn() -> usize.

默认情况下它调用 FnMut() -> usize 实现。

  • 疑点2:这里发生的是closures are Copy只要他们捕获的每个变量都是Copy,也就是说生成的闭包会被复制到f ,所以 f 最终得到它的 Copy。当您将 f 的定义改为采用 FnMut 时,您会收到错误消息,因为您面临与疑问 1 类似的情况:您正在尝试调用接收 &mut self 的函数当您将参数声明为 c: T 而不是 mut c: Tc: &mut T 时,在 FnMut 的眼中,两者都符合 &mut self 的条件。
  • 最后,疑惑3,self参数是闭包本身,它已经捕获或者移动一些变量到它自己,所以它现在拥有它们。