闭包类型
一个 闭包表达式 会产生一个闭包值,该值拥有一个无法写出的、独一无二的匿名类型。闭包类型大致等价于一个包含被捕获值的结构体。例如,下面的闭包
#![allow(unused)] fn main() { #[derive(Debug)] struct Point { x: i32, y: i32 } struct Rectangle { left_top: Point, right_bottom: Point } fn f<F : FnOnce() -> String> (g: F) { println!("{}", g()); } let mut rect = Rectangle { left_top: Point { x: 1, y: 1 }, right_bottom: Point { x: 0, y: 0 } }; let c = || { rect.left_top.x += 1; rect.right_bottom.x += 1; format!("{:?}", rect.left_top) }; f(c); // Prints "Point { x: 2, y: 1 }". }
会生成一个大致如下所示的闭包类型
// Note: This is not exactly how it is translated, this is only for
// illustration.
struct Closure<'a> {
left_top : &'a mut Point,
right_bottom_x : &'a mut i32,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.left_top.x += 1;
*self.right_bottom_x += 1;
format!("{:?}", self.left_top)
}
}
因此对 f 的调用就像是
f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.right_bottom.x });
捕获模式
一个 捕获模式 决定了如何将来自环境的 位置表达式 借用或移动到闭包中。捕获模式有
- 不可变借用(
ImmBorrow)—— 位置表达式被捕获为 共享引用。 - 唯一不可变借用(
UniqueImmBorrow)—— 这类似于不可变借用,但必须是唯一的,如下面 所述。 - 可变借用(
MutBorrow)—— 位置表达式被捕获为 可变引用。 - 移动(
ByValue)—— 位置表达式通过 移动值 的方式被捕获到闭包中。
来自环境的位置表达式根据其在闭包体内部的使用方式,按照兼容的优先顺序被捕获。捕获模式不受闭包周围代码的影响,例如相关变量或字段的生命周期,或闭包本身的生命周期。
Copy 值
实现了 Copy Trait 并被移动到闭包中的值会以 ImmBorrow 模式捕获。
#![allow(unused)] fn main() { let x = [0; 1024]; let c = || { let y = x; // x captured by ImmBorrow }; }
Async 输入捕获
Async 闭包总是捕获所有输入参数,无论它们是否在闭包体中使用。
捕获精度
一个 捕获路径 是一个序列,始于环境中的一个变量,后接零或多个应用于该变量的位置投影。
一个 位置投影 是应用于一个变量的 字段访问、元组索引、解引用(以及自动解引用),或 数组或切片索引 表达式。
闭包借用或移动捕获路径,该路径可能会根据下面描述的规则被截断。
例如
#![allow(unused)] fn main() { struct SomeStruct { f1: (i32, i32), } let s = SomeStruct { f1: (1, 2) }; let c = || { let x = s.f1.1; // s.f1.1 captured by ImmBorrow }; c(); }
这里的捕获路径是局部变量 s,后接字段访问 .f1,然后是元组索引 .1。这个闭包捕获了 s.f1.1 的不可变借用。
共享前缀
在捕获路径及其某个祖先都被闭包捕获的情况下,祖先路径会使用两者捕获模式中最高的模式进行捕获,即 CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode),使用严格弱序
ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
注意这可能需要递归应用。
#![allow(unused)] fn main() { // In this example, there are three different capture paths with a shared ancestor: fn move_value<T>(_: T){} let s = String::from("S"); let t = (s, String::from("T")); let mut u = (t, String::from("U")); let c = || { println!("{:?}", u); // u captured by ImmBorrow u.1.truncate(0); // u.0 captured by MutBorrow move_value(u.0.0); // u.0.0 captured by ByValue }; c(); }
总的来说,这个闭包将以 ByValue 方式捕获 u。
最右侧共享引用截断
如果捕获路径中的最右侧解引用是应用于共享引用的,则捕获路径会在该处被截断。
允许这种截断是因为通过共享引用读取的字段总是通过共享引用或复制来读取。当额外的精度从借用检查的角度看没有带来任何好处时,这有助于减小捕获的大小。
选择 最右侧 解引用的原因是避免不必要的更短生命周期。考虑以下示例
#![allow(unused)] fn main() { struct Int(i32); struct B<'a>(&'a i32); struct MyStruct<'a> { a: &'static Int, b: B<'a>, } fn foo<'a, 'b>(m: &'a MyStruct<'b>) -> impl FnMut() + 'static { let c = || drop(&m.a.0); c } }
如果这里捕获 m,那么闭包将不再比 'static 生命期长,因为 m 被限定为 'a。相反,它以 ImmBorrow 方式捕获 (*(*m).a)。
通配符模式绑定
闭包只捕获需要读取的数据。使用 通配符模式 绑定值不计为读取,因此不会被捕获。例如,以下闭包不会捕获 x
#![allow(unused)] fn main() { let x = String::from("hello"); let c = || { let _ = x; // x is not captured }; c(); let c = || match x { // x is not captured _ => println!("Hello World!") }; c(); }
这也包括元组、结构体和枚举的解构。与 RestPattern 或 StructPatternEtCetera 匹配的字段也不被视为读取,因此这些字段不会被捕获。以下是一些示例说明
#![allow(unused)] fn main() { let x = (String::from("a"), String::from("b")); let c = || { let (first, ..) = x; // captures `x.0` ByValue }; // The first tuple field has been moved into the closure. // The second tuple field is still accessible. println!("{:?}", x.1); c(); }
#![allow(unused)] fn main() { struct Example { f1: String, f2: String, } let e = Example { f1: String::from("first"), f2: String::from("second"), }; let c = || { let Example { f2, .. } = e; // captures `e.f2` ByValue }; // Field f2 cannot be accessed since it is moved into the closure. // Field f1 is still accessible. println!("{:?}", e.f1); c(); }
不支持对数组和切片进行部分捕获;即使使用通配符模式匹配、索引或子切片,整个切片或数组也总是会被捕获。例如
#![allow(unused)] fn main() { #[derive(Debug)] struct Example; let x = [Example, Example]; let c = || { let [first, _] = x; // captures all of `x` ByValue }; c(); println!("{:?}", x[1]); // ERROR: borrow of moved value: `x` }
与通配符匹配的值仍然必须已初始化。
#![allow(unused)] fn main() { let x: i32; let c = || { let _ = x; // ERROR: used binding `x` isn't initialized }; }
在 move 上下文中捕获引用
由于不允许从引用中移动字段,move 闭包只会捕获捕获路径中直到(但不包括)对引用进行的第一次解引用之前的那个前缀。引用本身将被移动到闭包中。
#![allow(unused)] fn main() { struct T(String, String); let mut t = T(String::from("foo"), String::from("bar")); let t_mut_ref = &mut t; let mut c = move || { t_mut_ref.0.push_str("123"); // captures `t_mut_ref` ByValue }; c(); }
裸指针解引用
由于解引用裸指针是 unsafe 的,闭包只会捕获捕获路径中直到(但不包括)对裸指针进行的第一次解引用之前的那个前缀。
#![allow(unused)] fn main() { struct T(String, String); let t = T(String::from("foo"), String::from("bar")); let t_ptr = &t as *const T; let c = || unsafe { println!("{}", (*t_ptr).0); // captures `t_ptr` by ImmBorrow }; c(); }
Union 字段
由于访问 Union 字段是 unsafe 的,闭包只会捕获捕获路径中直到 Union 本身之前的那个前缀。
#![allow(unused)] fn main() { union U { a: (i32, i32), b: bool, } let u = U { a: (123, 456) }; let c = || { let x = unsafe { u.a.0 }; // captures `u` ByValue }; c(); // This also includes writing to fields. let mut u = U { a: (123, 456) }; let mut c = || { u.b = true; // captures `u` with MutBorrow }; c(); }
引用非对齐的 struct
由于创建引用指向结构体中非对齐的字段是 未定义行为,因此闭包只会捕获捕获路径中直到(但不包括)对使用 packed 表示的结构体进行的第一次字段访问之前的那个前缀。这包括所有字段,即使是对齐的字段,以防止未来结构体中任何字段发生变化时出现兼容性问题。
#![allow(unused)] fn main() { #[repr(packed)] struct T(i32, i32); let t = T(2, 5); let c = || { let a = t.0; // captures `t` with ImmBorrow }; // Copies out of `t` are ok. let (a, b) = (t.0, t.1); c(); }
类似地,获取非对齐字段的地址也会捕获整个结构体
#![allow(unused)] fn main() { #[repr(packed)] struct T(String, String); let mut t = T(String::new(), String::new()); let c = || { let a = std::ptr::addr_of!(t.1); // captures `t` with ImmBorrow }; let a = t.0; // ERROR: cannot move out of `t.0` because it is borrowed c(); }
但如果结构体没有被打包(packed),则上述操作可行,因为它能精确捕获该字段
#![allow(unused)] fn main() { struct T(String, String); let mut t = T(String::new(), String::new()); let c = || { let a = std::ptr::addr_of!(t.1); // captures `t.1` with ImmBorrow }; // The move here is allowed. let a = t.0; c(); }
Box 与其他 Deref 实现的比较
Deref Trait 对于 Box 的实现与其他 Deref 实现的处理方式不同,因为它被视为一个特殊实体。
例如,我们来看看涉及 Rc 和 Box 的例子。*rc 被解糖为对定义在 Rc 上的 trait 方法 deref 的调用,但由于 *box 的处理方式不同,因此可以对 Box 的内容进行精确捕获。
非 move 闭包中的 Box
在非 move 闭包中,如果 Box 的内容没有被移动到闭包体中,那么 Box 的内容会被精确捕获。
#![allow(unused)] fn main() { struct S(String); let b = Box::new(S(String::new())); let c_box = || { let x = &(*b).0; // captures `(*b).0` by ImmBorrow }; c_box(); // Contrast `Box` with another type that implements Deref: let r = std::rc::Rc::new(S(String::new())); let c_rc = || { let x = &(*r).0; // captures `r` by ImmBorrow }; c_rc(); }
然而,如果 Box 的内容被移动到闭包中,那么整个 Box 将被捕获。这样做是为了最小化需要移动到闭包中的数据量。
#![allow(unused)] fn main() { // This is the same as the example above except the closure // moves the value instead of taking a reference to it. struct S(String); let b = Box::new(S(String::new())); let c_box = || { let x = (*b).0; // captures `b` with ByValue }; c_box(); }
move 闭包中的 Box
类似于在非 move 闭包中移动 Box 的内容,在 move 闭包中读取 Box 的内容会捕获整个 Box。
#![allow(unused)] fn main() { struct S(i32); let b = Box::new(S(10)); let c_box = move || { let x = (*b).0; // captures `b` with ByValue }; }
捕获中的唯一不可变借用
捕获可以通过一种特殊的借用方式发生,称为 唯一不可变借用,这种借用不能在语言的其他地方使用,也不能显式地写出来。它发生在修改可变引用的被引用对象时,如下面的示例所示
#![allow(unused)] fn main() { let mut b = false; let x = &mut b; let mut c = || { // An ImmBorrow and a MutBorrow of `x`. let a = &x; *x = true; // `x` captured by UniqueImmBorrow }; // The following line is an error: // let y = &x; c(); // However, the following is OK. let z = &x; }
在这种情况下,可变借用 x 是不可能的,因为 x 不是 mut 的。但同时,不可变借用 x 会使赋值非法,因为 & &mut 引用可能不是唯一的,因此不能安全地用于修改值。所以使用了唯一不可变借用:它不可变地借用 x,但像可变借用一样,它必须是唯一的。
在上面的示例中,取消注释 y 的声明会产生错误,因为它会违反闭包对 x 借用的唯一性;z 的声明是有效的,因为闭包的生命周期在块结束时已过期,释放了借用。
调用 Trait 和强制转换
所有闭包类型都实现了 FnOnce Trait,表明它们可以通过消耗闭包的所有权来调用一次。此外,一些闭包实现了更具体的调用 Trait
- 不会从任何捕获变量中移动出值的闭包实现了
FnMutTrait,表明它可以通过可变引用调用。
- 不会改变或移动出任何捕获变量的闭包实现了
FnTrait,表明它可以通过共享引用调用。
非捕获闭包 是指不从其环境中捕获任何内容的闭包。非异步、非捕获的闭包可以强制转换为具有匹配签名的函数指针(例如,fn())。
#![allow(unused)] fn main() { let add = |x, y| x + y; let mut x = add(5,7); type Binop = fn(i32, i32) -> i32; let bo: Binop = add; x = bo(5,7); }
Async 闭包 Trait
Async 闭包在是否实现 FnMut 或 Fn 方面有进一步的限制。
Async 闭包返回的 Future 具有与普通闭包类似的捕获特性。它根据在 async 闭包中的使用方式捕获来自 async 闭包的位置表达式。如果 async 闭包具有以下任一属性,则称其对其 Future 是 出借的
Future包含一个可变捕获。- Async 闭包以值方式捕获,但通过解引用投影访问值的情况除外。
如果 async 闭包对其 Future 是出借的,则 不 实现 FnMut 和 Fn。总是实现 FnOnce。
示例:可变捕获的第一条规则可以通过以下示例说明
#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {} fn f() { let mut x = 1i32; let c = async || { x = 2; // x captured with MutBorrow }; takes_callback(c); // ERROR: async closure does not implement `FnMut` } }普通值捕获的第二条规则可以通过以下示例说明
#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = x + 2; // x captured ByValue }; takes_callback(c); // ERROR: async closure does not implement `Fn` } }第二条规则的例外情况可以通过使用解引用来示例说明,这确实允许实现
Fn和FnMut#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = *x + 2; }; takes_callback(c); // OK: implements `Fn` } }
Async 闭包以与普通闭包实现 Fn、FnMut 和 FnOnce 类似的方式实现了 AsyncFn、AsyncFnMut 和 AsyncFnOnce;也就是说,取决于在其闭包体中使用捕获变量的方式。
其他 Trait
所有闭包类型都实现了 Sized Trait。此外,如果其存储的捕获值的类型允许,闭包类型会实现以下 Trait
Send 和 Sync 的规则与普通结构体类型相同,而 Clone 和 Copy 的行为类似于 派生 的实现。对于 Clone,捕获值克隆的顺序未指定。
由于捕获通常是通过引用进行的,因此产生了以下一般规则
- 如果所有捕获的值都是
Sync的,那么闭包就是Sync的。 - 如果所有通过非唯一不可变引用捕获的值是
Sync的,并且所有通过唯一不可变或可变引用、复制或移动捕获的值是Send的,那么闭包就是Send的。 - 如果闭包不通过唯一不可变或可变引用捕获任何值,并且所有通过复制或移动捕获的值分别是
Clone或Copy的,那么闭包就是Clone或Copy的。
Drop 顺序
如果闭包以值方式捕获结构体、元组和枚举等复合类型的一个字段,该字段的生命周期将与闭包绑定。因此,复合类型的不同字段可能在不同时间被 Drop。
#![allow(unused)] fn main() { { let tuple = (String::from("foo"), String::from("bar")); // --+ { // | let c = || { // ----------------------------+ | // tuple.0 is captured into the closure | | drop(tuple.0); // | | }; // | | } // 'c' and 'tuple.0' dropped here ------------+ | } // tuple.1 dropped here -----------------------------+ }
2018 及之前版本
闭包类型差异
在 2018 及之前版本中,闭包总是完整捕获整个变量,而不是精确的捕获路径。这意味着对于 闭包类型 部分使用的示例,生成的闭包类型会像这样
struct Closure<'a> {
rect : &'a mut Rectangle,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.rect.left_top.x += 1;
self.rect.right_bottom.x += 1;
format!("{:?}", self.rect.left_top)
}
}
并且对 f 的调用会按如下方式工作
f(Closure { rect: rect });
捕获精度差异
结构体、元组和枚举等复合类型总是被完整捕获,而不是按单个字段捕获。因此,可能需要借用到局部变量才能捕获单个字段
#![allow(unused)] fn main() { use std::collections::HashSet; struct SetVec { set: HashSet<u32>, vec: Vec<u32> } impl SetVec { fn populate(&mut self) { let vec = &mut self.vec; self.set.iter().for_each(|&n| { vec.push(n); }) } } }
相反,如果闭包直接使用 self.vec,它将尝试以可变引用方式捕获 self。但由于 self.set 已经被借用来进行迭代,代码将无法编译。
如果使用了 move 关键字,那么所有捕获都将通过移动方式进行,或者对于 Copy 类型,通过复制方式进行,无论借用是否可行。move 关键字通常用于让闭包的生命周期长于被捕获值,例如闭包被返回或用于生成新线程时。
无论数据是否会被闭包读取(例如在通配符模式下),如果在闭包中提及了在闭包外部定义的变量,该变量将被完整捕获。
Drop 顺序差异
由于复合类型被完整捕获,以值方式捕获其中一种复合类型的闭包会在闭包被 Drop 的同时 Drop 整个被捕获的变量。
#![allow(unused)] fn main() { { let tuple = (String::from("foo"), String::from("bar")); { let c = || { // --------------------------+ // tuple is captured into the closure | drop(tuple.0); // | }; // | } // 'c' and 'tuple' dropped here ------------+ } }