【问题标题】:Ownership, closures, FnOnce: much confusion所有权、关闭、FnOnce:非常混乱
【发布时间】:2019-11-06 16:45:26
【问题描述】:

我有以下sn-p的代码:

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

【问题讨论】:

  • 你读过编译器信息吗?
  • 虽然消息说让 g 可变并且确实允许代码编译,但为什么不可变的 g 可以在 f 内部使用而不能在外部使用?
  • 因为FnOnce 具有内部可变性。
  • @Stargateur "FnOnce 具有内部可变性" – 不,这不是真的。调用FnOnce消耗闭包,但这与内部可变性无关。
  • @Stargateur “内部可变性”是行话。它具有特定的、明确的含义。当然,您可以随意使用该术语来表示其他含义,但我认为这对学习 Rust 的人没有帮助。

标签: rust closures move mutable borrowing


【解决方案1】:

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

如果您将闭包定义为

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

编译器将推断闭包的类型为FnMut。虽然闭包返回了拥有的变量x,但它仍然可以被多次调用,因为xCopy,所以编译器选择FnMut作为最普遍适用的类型。

当调用FnMut 闭包时,闭包本身是通过可变引用传递的。这解释了您的第一个问题 – 直接调用 g 不起作用,除非您将其设为可变,否则您无法对其进行可变引用。我在这里也含蓄地回答了你的第三个问题——Fn 特征的调用方法中的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 传入,因此调用 消耗 闭包。你可以放弃你拥有的任何价值的所有权——它不需要是可变的。

通过f()调用g可以多次调用的原因是g就是Copy。它只捕获一个整数,因此可以根据需要复制任意多次。对f() 的每次调用都会创建一个g 的新副本,在f() 内部调用它时会使用该副本。

【讨论】:

    【解决方案2】:

    需要一些关于 Fn* 特征家族的基础知识才能理解闭包的实际工作原理。你有以下特点:

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

    解决您的个人疑问:

    • 疑点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() -&gt; usize 实现。

    • 疑点2:这里发生的事情是closures are Copy只要它们捕获的每个变量都是Copy,这意味着生成的闭包将被复制到f中,这样f就结束了取一个Copy。当您将 f 的定义更改为采用 FnMut 时,您会收到错误,因为您面临与怀疑 1 类似的情况:您正在尝试调用接收 &amp;mut self 的函数,而您已声明参数为c: T,而不是mut c: Tc: &amp;mut T,在FnMut 眼中,这两者都符合&amp;mut self
    • 最后,疑问 3,self 参数是闭包本身,它已经捕获或移动了一些变量到自身中,因此它现在拥有它们。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-10-27
      • 1970-01-01
      • 2011-03-01
      • 2018-11-30
      • 1970-01-01
      • 1970-01-01
      • 2016-11-19
      • 1970-01-01
      相关资源
      最近更新 更多