所有权、关闭、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
self
在Fn
、FnMut
和FnOnce
特征定义中指的是什么?那是闭包本身吗?还是环境?
例如。来自文档:
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
您在这里处理两种不同类型的闭包 – FnOnce
和 FnMut
。两种类型的闭包都有不同的调用约定。
如果您将闭包定义为
let mut x = 32;
let g = move || {
x = 33;
x
};
编译器会将闭包类型推断为 FnMut
。虽然闭包 returns 拥有变量 x
,但它仍然可以被多次调用,因为 x
是 Copy
,所以编译器选择 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
可以多次调用,是因为g
是Copy
。它只捕获一个整数,因此可以根据需要复制任意多次。每次调用 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: T
或 c: &mut T
时,在 FnMut
的眼中,两者都符合 &mut self
的条件。
- 最后,疑惑3,
self
参数是闭包本身,它已经捕获或者移动一些变量到它自己,所以它现在拥有它们。
我有以下代码片段:
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
self
在Fn
、FnMut
和FnOnce
特征定义中指的是什么?那是闭包本身吗?还是环境?
例如。来自文档:
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
您在这里处理两种不同类型的闭包 – FnOnce
和 FnMut
。两种类型的闭包都有不同的调用约定。
如果您将闭包定义为
let mut x = 32;
let g = move || {
x = 33;
x
};
编译器会将闭包类型推断为 FnMut
。虽然闭包 returns 拥有变量 x
,但它仍然可以被多次调用,因为 x
是 Copy
,所以编译器选择 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
可以多次调用,是因为g
是Copy
。它只捕获一个整数,因此可以根据需要复制任意多次。每次调用 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: T
或c: &mut T
时,在FnMut
的眼中,两者都符合&mut self
的条件。 - 最后,疑惑3,
self
参数是闭包本身,它已经捕获或者移动一些变量到它自己,所以它现在拥有它们。