所有权、闭包、FnOnce:很多混淆

3

我有以下代码片段:

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

FnFnMutFnOnce 这些 traits 的定义中,self 是指什么?它是指闭包本身还是指“环境”? 例如,从文档中:

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

所有似乎都与特质声明中的 self 变量有关(疑问 3):FnOnceself,但 FnMut&mut self - fcracker79
请查看 https://doc.rust-lang.org/book/ch13-01-closures.html 上的相关编程内容。 - Stargateur
1
FnOnce 具有内部可变性”- 不,这不是真的。调用 FnOnce消费 闭包,但这与内部可变性无关。 - Sven Marnach
@SvenMarnach,那是我看待它的方式。 - Stargateur
3
“内部可变性”是术语。它有一个明确的定义。当然,你可以自由地用这个术语来表示其他意思,但我认为这对学习Rust的人没有帮助。 - Sven Marnach
显示剩余4条评论
2个回答

5

了解Fn*特质族的基础知识是理解闭包实际工作原理的必要条件。您拥有以下特质:

  • 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:这里发生的情况是,只要每个被捕获的变量都是Copy,那么闭包就会被Copy,生成的闭包将被复制到f中,以便f最终获取它的一个Copy。当您将f的定义更改为使用FnMut时,您会遇到错误,因为您面临与疑惑1类似的情况:您试图调用一个接收&mut self的函数,而您已经声明了参数为c: T,而不是任一mut c: Tc:& mut T,其中任何一种都符合FnMut的要求。
  • 最后,疑惑3,self参数是闭包本身,它已经捕获或移动了一些变量到自己内部,因此现在拥有它们。

3
你在这里面处理两种不同类型的闭包 - FnOnceFnMut。这两种类型的闭包有不同的调用约定。
如果你将你的闭包定义为:
let mut x = 32;
let g  = move || {
    x = 33;
    x
};

编译器将推断闭包的类型为FnMut。尽管闭包返回所拥有的变量x,但它仍然可以被调用多次,因为xCopy类型,所以编译器选择FnMut作为最通用的适用类型。
当调用FnMut闭包时,闭包本身通过可变引用传递。这解释了你的第一个问题——直接调用g是不起作用的,除非你使其可变,否则无法对其进行可变引用。我在这里隐含地回答了你的第三个问题——Fn特质中的call方法中的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;
}

在这种情况下,闭包通过值传递,因此调用会消耗闭包。您可以放弃拥有的任何值的所有权-它不需要可变性即可工作。
当通过f()调用g时,您可以多次调用g的原因是g是Copy。它只捕获一个整数,因此可以随意复制。每次调用f()都会创建g的新副本,在f()内部调用时被使用。

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