13-闭包类型
14 分钟阅读
译文 · 基于 The Rust Reference
原文链接: https://doc.rust-lang.org/reference/types/closure.html
r[type.closure]
闭包类型
r[type.closure.intro] 闭包表达式产生一个具有无法写出的唯一匿名类型的闭包值。闭包类型大致等价于包含被捕获值的结构体。例如,下面的闭包:
| |
会生成大致如下的闭包类型:
| |
因此对 f 的调用工作起来就像:
| |
r[type.closure.capture]
捕获模式
r[type.closure.capture.intro] 捕获模式 决定环境中的位置表达式如何被借用或移动到闭包中。捕获模式有:
- 不可变借用(
ImmBorrow)—— 位置表达式被捕获为共享引用。 - 唯一不可变借用(
UniqueImmBorrow)—— 这类似于不可变借用,但必须如下文所述那样是唯一的。 - 可变借用(
MutBorrow)—— 位置表达式被捕获为可变引用。 - 移动(
ByValue)—— 位置表达式通过移动该值被捕获到闭包中。
r[type.closure.capture.precedence] 环境中的位置表达式从与该被捕获值在闭包体内使用方式兼容的第一种模式被捕获。该模式不受闭包周围代码的影响,例如所涉及变量或字段的生命周期,或闭包自身的生命周期。
r[type.closure.capture.copy]
Copy 值
实现了 Copy 且被移动到闭包中的值以 ImmBorrow 模式捕获。
| |
r[type.closure.async.input]
Async 输入捕获
Async 闭包总是捕获所有输入参数,无论它们是否在函数体内被使用。
捕获精度
r[type.closure.capture.precision.capture-path] 捕获路径 是以环境中的一个变量开始、后跟零个或多个来自该变量的位置投影的序列。
r[type.closure.capture.precision.place-projection] 位置投影 是应用于变量的字段访问、元组索引、解引用(以及自动解引用)、数组或切片索引表达式,或模式解构。
注意 在
rustc中,模式解构脱糖为一系列解引用以及字段或元素访问。
r[type.closure.capture.precision.intro] 闭包借用或移动捕获路径,该路径可能根据下述规则被截断。
例如:
| |
此处捕获路径是局部变量 s,后跟字段访问 .f1,然后是元组索引 .1。此闭包捕获对 s.f1.1 的不可变借用。
r[type.closure.capture.precision.shared-prefix]
共享前缀
当捕获路径以及该路径的某个祖先都被闭包捕获时,祖先路径以两种捕获中最高的捕获模式捕获,CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode),使用严格弱序:
ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
注意这可能需要递归应用。
| |
总体而言,此闭包将以 ByValue 捕获 u。
r[type.closure.capture.precision.dereference-shared]
最右侧共享引用截断
若捕获路径中的解引用应用于共享引用,则捕获路径在该路径中最右侧的解引用处被截断。
允许这种截断是因为通过共享引用读取的字段将始终通过共享引用或副本读取。这有助于在额外精度从借用检查角度看并无收益时减小捕获的大小。
之所以是 最右侧 的解引用,是为了帮助避免不必要地更短的生命周期。考虑下面的例子:
| |
若这捕获 m,则闭包将不再长于 'static,因为 m 受 'a 约束。相反,它以 ImmBorrow 捕获 (*(*m).a)。
r[type.closure.capture.precision.wildcard]
通配符模式绑定
r[type.closure.capture.precision.wildcard.reads] 闭包只捕获需要被读取的数据。用通配符模式绑定值不会读取该值,因此该位置不被捕获。
| |
r[type.closure.capture.precision.wildcard.destructuring] 解构元组、结构体和单变体枚举本身不会导致读取或捕获该位置。
注意 标有 [
#[non_exhaustive]][attributes.type-system.non_exhaustive] 的枚举始终被视为具有多个变体。参见 [type.closure.capture.precision.discriminants.non_exhaustive]。
| |
r[type.closure.capture.precision.wildcard.fields]
与 [RestPattern](..)或 [StructPatternEtCetera](也是 ..)匹配的字段不被读取,这些字段也不被捕获。
| |
r[type.closure.capture.precision.wildcard.array-slice] 不支持对数组和切片的部分捕获;即使与通配符模式匹配、索引或子切片一起使用,也总是捕获整个切片或数组。
| |
r[type.closure.capture.precision.wildcard.initialized] 用通配符匹配的值仍然必须已初始化。
| |
r[type.closure.capture.precision.discriminants]
为读取判别式而捕获
r[type.closure.capture.precision.discriminants.reads]
若模式匹配读取判别式,则包含该判别式的位置以 ImmBorrow 捕获。
r[type.closure.capture.precision.discriminants.multiple-variant]
匹配具有多于一个变体的枚举的某个变体会读取判别式,以 ImmBorrow 捕获该位置。
| |
| |
r[type.closure.capture.precision.discriminants.single-variant] 匹配单变体枚举的唯一变体不会读取判别式,也不会捕获该位置。
| |
r[type.closure.capture.precision.discriminants.non_exhaustive]
若对枚举应用了 [#[non_exhaustive]][attributes.type-system.non_exhaustive],则就决定是否发生读取而言,该枚举被视为具有多个变体,即使它实际上只有一个变体。
r[type.closure.capture.precision.discriminants.uninhabited-variants] 即使除正在匹配的变体之外的所有变体都无居住值,使该模式[不可反驳][patterns.refutable],只要在其他情况下会读取判别式,就仍然会读取。
| |
r[type.closure.capture.precision.range-patterns]
捕获与范围模式
r[type.closure.capture.precision.range-patterns.reads]
匹配[范围模式][patterns.range]会读取被匹配的位置,即使该范围包含该类型的所有可能值,并以 ImmBorrow 捕获该位置。
| |
r[type.closure.capture.precision.slice-patterns]
捕获与切片模式
r[type.closure.capture.precision.slice-patterns.slices]
将切片与[切片模式][patterns.slice]匹配(除了只有单个[剩余模式][patterns.rest]的模式,即 [..])被视为从切片读取长度,并以 ImmBorrow 捕获该切片。
| |
| |
注意 或许令人惊讶的是,尽管长度包含在指向切片的(宽)指针 中,被视为被读取并被捕获的是 被指对象(切片)的位置。
1 2 3 4 5 6 7 8 9fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { // 闭包长于 `'l`,因为它捕获 `**x`。若 // 它捕获的是 `*x`,它将活得不够长, // 无法满足 `impl Fn() + 'l` 约束。 || match *x { // 以 `ImmBorrow` 捕获 `**x`。 &[] => (), _ => (), } }这样,该行为与在被匹配者中解引用到切片是一致的。
1 2 3 4 5 6fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { || match **x { // 以 `ImmBorrow` 捕获 `**x`。 [] => (), _ => (), } }细节参见 Rust PR #138961。
r[type.closure.capture.precision.slice-patterns.arrays] 由于数组的长度由其类型固定,将数组与切片模式匹配本身不会捕获该位置。
| |
r[type.closure.capture.precision.move-dereference]
在 move 上下文中捕获引用
因为不允许从引用中移出字段,move 闭包只会捕获捕获路径中直到但不包括第一次解引用引用的前缀。引用本身将被移动到闭包中。
| |
r[type.closure.capture.precision.raw-pointer-dereference]
裸指针解引用
因为解引用裸指针是 unsafe 的,闭包只会捕获捕获路径中直到但不包括第一次解引用裸指针的前缀。
| |
r[type.closure.capture.precision.union]
联合体字段
因为访问联合体字段是 unsafe 的,闭包只会捕获捕获路径中直到联合体本身的前缀。
| |
r[type.closure.capture.precision.unaligned]
指向未对齐 struct 的引用
因为创建指向结构体中未对齐字段的引用是未定义行为,闭包只会捕获捕获路径中直到但不包括第一次进入使用packed 表示的结构体的字段访问的前缀。这包括所有字段,即使是对齐的字段,以防止将来结构体中任何字段发生变化时的兼容性问题。
| |
类似地,取未对齐字段的地址也会捕获整个结构体:
| |
但若它不是 packed 的,上面的代码可以工作,因为它精确捕获该字段:
| |
r[type.closure.capture.precision.box-deref]
Box 与其他 Deref 实现
Box 的 Deref trait 实现与其他 Deref 实现的处理方式不同,因为它被视为特殊实体。
例如,让我们看看涉及 Rc 和 Box 的例子。*rc 脱糖为对 Rc 上定义的 trait 方法 deref 的调用,但由于 *box 被区别对待,可以精确捕获 Box 的内容。
r[type.closure.capture.precision.box-non-move.not-moved]
非 move 闭包中的 Box
在非 move 闭包中,若 Box 的内容未被移动到闭包体中,则精确捕获 Box 的内容。
| |
r[type.closure.capture.precision.box-non-move.moved]
然而,若 Box 的内容被移动到闭包中,则整个 box 被捕获。这样做是为了最小化需要移动到闭包中的数据量。
| |
r[type.closure.capture.precision.box-move.read]
move 闭包中的 Box
与在非 move 闭包中移动 Box 的内容类似,在 move 闭包中读取 Box 的内容将捕获整个 Box。
| |
r[type.closure.unique-immutable]
捕获中的唯一不可变借用
捕获可以通过一种称为 唯一不可变借用 的特殊借用发生,它不能在语言的其他任何地方使用,也无法显式写出。它发生在修改可变引用的所指对象时,如下面的例子:
| |
在这种情况下,可变地借用 x 是不可能的,因为 x 不是 mut 的。但与此同时,不可变地借用 x 会使赋值非法,因为 & &mut 引用可能不是唯一的,因此不能安全地用于修改值。于是使用唯一不可变借用:它不可变地借用 x,但像可变借用一样,它必须是唯一的。
在上面的例子中,取消注释 y 的声明会产生错误,因为它会违反闭包对 x 借用的唯一性;z 的声明是合法的,因为闭包的生命周期在块结束时已到期,释放了该借用。
r[type.closure.call]
调用 trait 与强制转换
r[type.closure.call.intro]
所有闭包类型都实现 [FnOnce],表明它们可以通过消耗闭包的所有权被调用一次。此外,一些闭包实现更具体的调用 trait:
r[type.closure.call.fn-mut]
- 不从任何被捕获变量中移出的闭包实现 [
FnMut],表明它可以通过可变引用被调用。
r[type.closure.call.fn]
- 不修改或从任何被捕获变量中移出的闭包实现 [
Fn],表明它可以通过共享引用被调用。
注意
move闭包仍然可能实现 [Fn] 或 [FnMut],即使它们按移动捕获变量。这是因为闭包类型所实现的 trait 由闭包对被捕获值做了什么决定,而不是由它如何捕获它们决定。
r[type.closure.non-capturing]
不捕获的闭包 是不从其环境中捕获任何东西的闭包。非 async、不捕获的闭包可以被强制转换为具有匹配签名的函数指针(例如 fn())。
| |
r[type.closure.async.traits]
Async 闭包 trait
r[type.closure.async.traits.fn-family]
Async 闭包在是否实现 [FnMut] 或 [Fn] 上有进一步的限制。
Async 闭包返回的 [Future] 具有与闭包类似的捕获特征。它根据位置表达式的使用方式从 async 闭包中捕获它们。若 async 闭包具有以下性质之一,则称它对其 [Future] 出借:
- 该
Future包含可变捕获。 - 该 async 闭包按值捕获,除非该值通过解引用投影访问。
若 async 闭包对其 Future 出借,则 不 实现 [FnMut] 和 [Fn]。始终实现 [FnOnce]。
示例:关于可变捕获的第一条可以用下面说明:
1 2 3 4 5 6 7 8 9fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {} fn f() { let mut x = 1i32; let c = async || { x = 2; // x 以 MutBorrow 捕获 }; takes_callback(c); // 错误:async 闭包未实现 `FnMut` }关于常规值捕获的第二条可以用下面说明:
1 2 3 4 5 6 7 8 9fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = x + 2; // x 以 ByValue 捕获 }; takes_callback(c); // 错误:async 闭包未实现 `Fn` }第二条的例外可以通过使用解引用来说明,这确实允许实现
Fn和FnMut:
1 2 3 4 5 6 7 8 9fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = *x + 2; }; takes_callback(c); // 正确:实现 `Fn` }
r[type.closure.async.traits.async-family]
Async 闭包以实现 [Fn]、[FnMut] 和 [FnOnce] 的类似方式实现 [AsyncFn]、[AsyncFnMut] 和 [AsyncFnOnce];也就是说,取决于其函数体中对被捕获变量的使用。
r[type.closure.traits]
其他 trait
r[type.closure.traits.intro]
所有闭包类型都实现 Sized。此外,若其所存储捕获的类型允许,闭包类型还实现以下 trait:
r[type.closure.traits.behavior]
Send 和 Sync 的规则与普通结构体类型匹配,而 Clone 和 Copy 的行为如同派生。对于 Clone,被捕获值的克隆顺序未指定。
由于捕获常常通过引用进行,产生以下一般规则:
- 若所有被捕获值都是
Sync的,则闭包是Sync的。 - 若所有通过非唯一不可变引用捕获的值都是
Sync的,并且所有通过唯一不可变或可变引用、复制或移动捕获的值都是Send的,则闭包是Send的。 - 若闭包不以唯一不可变或可变引用捕获任何值,并且它通过复制或移动捕获的所有值分别是
Clone或Copy的,则闭包是Clone或Copy的。
r[type.closure.drop-order]
Drop 顺序
若闭包按值捕获复合类型(如结构体、元组和枚举)的某个字段,则该字段的生命周期现在将与闭包绑定。因此,复合类型的不相交字段有可能在不同时间被 drop。
| |
r[type.closure.capture.precision.edition2018.entirety]
2018 edition 及之前
闭包类型的差异
在 2018 edition 及之前,闭包总是捕获整个变量,而不使用其精确捕获路径。这意味着对于闭包类型一节中使用的例子,生成的闭包类型会看起来大致如下:
| |
而对 f 的调用会如下工作:
| |
r[type.closure.capture.precision.edition2018.composite]
捕获精度的差异
复合类型如结构体、元组和枚举总是被整体捕获,而不是按各个字段。因此,为了捕获单个字段,可能有必要先借用到局部变量:
| |
若闭包改为直接使用 self.vec,则它会试图以可变引用捕获 self。但由于 self.set 已经被借用来迭代,代码将无法编译。
r[type.closure.capture.precision.edition2018.move]
若使用了 move 关键字,则所有捕获都是按移动,或者对于 Copy 类型按复制,无论借用是否可行。move 关键字通常用于允许闭包长于被捕获的值,例如当闭包被返回或用于生成新线程时。
r[type.closure.capture.precision.edition2018.wildcard] 无论数据是否会被闭包读取,即在通配符模式的情况下,若在闭包中提及在闭包外定义的变量,该变量就会被整体捕获。
r[type.closure.capture.precision.edition2018.drop-order]
Drop 顺序的差异
由于复合类型被整体捕获,按值捕获其中一种复合类型的闭包会在闭包被 drop 的同时 drop 整个被捕获变量。
| |