闭包类型
闭包表达式会产生一个闭包值,其类型是唯一的匿名类型,无法显式写出。闭包类型大致等同于一个包含捕获值的结构体。例如,以下闭包
#![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
的调用就像是
// Note: This is not valid Rust due to the duplicate mutable borrows.
// This is only provided as an illustration.
f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.left_top.x });
捕获模式
捕获模式 决定了如何将环境中的 位置表达式 借用或移动到闭包中。捕获模式包括:
- 不可变借用 (
ImmBorrow
) — 位置表达式以 共享引用 的形式捕获。 - 唯一不可变借用 (
UniqueImmBorrow
) — 这类似于不可变借用,但必须是唯一的,如下文 所述。 - 可变借用 (
MutBorrow
) — 位置表达式以 可变引用 的形式捕获。 - 移动 (
ByValue
) — 位置表达式通过 移动值 到闭包中进行捕获。
环境中的位置表达式会根据与闭包体内部捕获值的使用方式兼容的首个模式进行捕获。模式不受闭包周围的代码影响,例如涉及变量或字段的生命周期,或闭包本身的生命周期。
Copy
值
实现了 Copy
特征并移动到闭包中的值,会使用 ImmBorrow
模式进行捕获。
#![allow(unused)] fn main() { let x = [0; 1024]; let c = || { let y = x; // x captured by ImmBorrow }; }
异步输入捕获
异步闭包总是捕获所有输入参数,无论它们是否在闭包体内部使用。
捕获精度
捕获路径 是一个序列,以环境中的变量开头,后跟零个或多个应用于该变量的位置投影。
位置投影 是应用于变量的 字段访问、元组索引、解引用(和自动解引用)或 数组或切片索引 表达式。
闭包借用或移动捕获路径,捕获路径可能会根据下述规则进行截断。
例如
#![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
闭包只会捕获捕获路径的前缀,该前缀一直到但不包括对引用的首次解引用。引用本身将被移动到闭包中。
#![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(); }
联合体字段
由于访问联合体字段是 unsafe
的,闭包只会捕获捕获路径的前缀,该前缀一直到联合体本身。
#![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
实现
对于 Box
的 Deref
特征的实现与其他的 Deref
实现不同对待,因为它被认为是特殊的实体。
例如,让我们看一下涉及 Rc
和 Box
的例子。*rc
会被脱糖为对 Rc
上定义的特征方法 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
的声明是有效的,因为闭包的生命周期在块的末尾已过期,释放了借用。
调用特征和强制转换
闭包类型都实现了 FnOnce
,表明它们可以通过消耗闭包的所有权来调用一次。此外,一些闭包实现了更具体的调用特征:
- 一个不移出任何捕获变量的闭包实现了
FnMut
,表明它可以通过可变引用调用。
- 一个不改变或移出任何捕获变量的闭包实现了
Fn
,表明它可以通过共享引用调用。
注意:
move
闭包仍然可以实现Fn
或FnMut
,即使它们通过移动捕获变量。这是因为闭包类型实现的特征是由闭包如何处理捕获的值决定的,而不是它如何捕获它们。
非捕获闭包 是指不从其环境中捕获任何内容的闭包。非异步、非捕获闭包可以强制转换为具有匹配签名的函数指针(例如,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); }
异步闭包特征
异步闭包对其是否实现 FnMut
或 Fn
有进一步的限制。
异步闭包返回的 Future
具有与闭包类似的捕获特性。它根据位置表达式在异步闭包中的使用方式来捕获它们。如果异步闭包具有以下任一属性,则称其借用其 Future
:
Future
包含可变捕获。- 异步闭包按值捕获,除非通过解引用投影访问该值。
如果异步闭包借用其 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` } }
异步闭包以类似于常规闭包实现 Fn
、FnMut
和 FnOnce
的方式实现 AsyncFn
、AsyncFnMut
和 AsyncFnOnce
;也就是说,取决于捕获的变量在其主体中的使用方式。
其他特征
所有闭包类型都实现 Sized
。此外,如果其存储的捕获类型允许,闭包类型还会实现以下特征:
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 ------------+ } }