闭包类型
闭包表达式产生具有唯一匿名类型的闭包值,该类型无法写出。闭包类型大致等同于包含捕获值的结构体。例如,以下闭包:
#![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 并被移动到闭包中的值使用 ImmBorrow 模式捕获。
#![allow(unused)]
fn main() {
let x = [0; 1024];
let c = || {
let y = x; // x captured by ImmBorrow
};
}
异步输入捕获
异步闭包始终捕获所有输入参数,无论它们是否在主体中使用。
捕获精度
捕获路径是从环境中的变量开始的序列,后跟零个或多个从该变量的位置投影。
位置投影是应用于变量的字段访问、元组索引、解引用(和自动解引用)、数组或切片索引表达式或模式解构。
Note
在
rustc中,模式解构被脱糖为一系列解引用和字段或元素访问。
闭包借用或移动捕获路径,该路径可能会根据下面描述的规则被截断。
例如:
#![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.1 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)。
通配符模式绑定
闭包只捕获需要读取的数据。使用通配符模式绑定值不会读取值,因此不会捕获该位置。
#![allow(unused)]
fn main() {
struct S; // A non-`Copy` type.
let x = S;
let c = || {
let _ = x; // Does not capture `x`.
};
let c = || match x {
_ => (), // Does not capture `x`.
};
x; // OK: `x` can be moved here.
c();
}
解构元组、结构体和单变体枚举本身不会导致读取或捕获位置。
Note
标记了
#[non_exhaustive]的枚举始终被视为具有多个变体。参见 type.closure.capture.precision.discriminants.non_exhaustive。
#![allow(unused)]
fn main() {
struct S; // A non-`Copy` type.
// Destructuring tuples does not cause a read or capture.
let x = (S,);
let c = || {
let (..) = x; // Does not capture `x`.
};
x; // OK: `x` can be moved here.
c();
// Destructuring unit structs does not cause a read or capture.
let x = S;
let c = || {
let S = x; // Does not capture `x`.
};
x; // OK: `x` can be moved here.
c();
// Destructuring structs does not cause a read or capture.
struct W<T>(T);
let x = W(S);
let c = || {
let W(..) = x; // Does not capture `x`.
};
x; // OK: `x` can be moved here.
c();
// Destructuring single-variant enums does not cause a read
// or capture.
enum E<T> { V(T) }
let x = E::V(S);
let c = || {
let E::V(..) = x; // Does not capture `x`.
};
x; // OK: `x` can be moved here.
c();
}
与 RestPattern(..)或 StructPatternEtCetera(也是 ..)匹配的字段不会被读取,并且这些字段不会被捕获。
#![allow(unused)]
fn main() {
struct S; // A non-`Copy` type.
let x = (S, S);
let c = || {
let (x0, ..) = x; // Captures `x.0` by `ByValue`.
};
// Only the first tuple field was captured by the closure.
x.1; // OK: `x.1` can be moved here.
c();
}
不支持数组和切片的部分捕获;即使使用通配符模式匹配、索引或子切片,也始终捕获整个切片或数组。
#![allow(unused)]
fn main() {
struct S; // A non-`Copy` type.
let mut x = [S, S];
let c = || {
let [x0, _] = x; // Captures all of `x` by `ByValue`.
};
let _ = &mut x[1]; // ERROR: Borrow of moved value.
}
使用通配符匹配的值仍然必须被初始化。
#![allow(unused)]
fn main() {
let x: u8;
let c = || {
let _ = x; // ERROR: Binding `x` isn't initialized.
};
}
判别值读取的捕获
如果模式匹配读取判别值,则包含该判别值的位置通过 ImmBorrow 被捕获。
匹配具有多个变体的枚举的变体会读取判别值,通过 ImmBorrow 捕获该位置。
#![allow(unused)]
fn main() {
struct S; // A non-`Copy` type.
let mut x = (Some(S), S);
let c = || match x {
(None, _) => (),
// ^^^^
// This pattern requires reading the discriminant, which
// causes `x.0` to be captured by `ImmBorrow`.
_ => (),
};
let _ = &mut x.0; // ERROR: Cannot borrow `x.0` as mutable.
// ^^^
// The closure is still live, so `x.0` is still immutably
// borrowed here.
c();
}
#![allow(unused)]
fn main() {
struct S; // A non-`Copy` type.
let x = (Some(S), S);
let c = || match x { // Captures `x.0` by `ImmBorrow`.
(None, _) => (),
_ => (),
};
// Though `x.0` is captured due to the discriminant read,
// `x.1` is not captured.
x.1; // OK: `x.1` can be moved here.
c();
}
匹配单变体枚举的唯一变体不会读取判别值,也不会捕获该位置。
#![allow(unused)]
fn main() {
enum E<T> { V(T) } // A single-variant enum.
let x = E::V(());
let c = || {
let E::V(_) = x; // Does not capture `x`.
};
x; // OK: `x` can be moved here.
c();
}
如果 #[non_exhaustive] 应用于枚举,则为了决定是否发生读取,该枚举被视为具有多个变体,即使它实际上只有一个变体。
即使除了正在匹配的变体之外的所有变体都是无人居住的,使得模式是不可反驳的,如果判别值在其他情况下会被读取,则仍然会被读取。
#![allow(unused)]
fn main() {
enum Empty {}
let mut x = Ok::<_, Empty>(42);
let c = || {
let Ok(_) = x; // Captures `x` by `ImmBorrow`.
};
let _ = &mut x; // ERROR: Cannot borrow `x` as mutable.
c();
}
捕获和范围模式
匹配[范围模式]range pattern会读取被匹配的位置,即使该范围包含类型的所有可能值,并通过 ImmBorrow 捕获该位置。
#![allow(unused)]
fn main() {
let mut x = 0u8;
let c = || {
let 0..=u8::MAX = x; // Captures `x` by `ImmBorrow`.
};
let _ = &mut x; // ERROR: Cannot borrow `x` as mutable.
c();
}
捕获和切片模式
将切片与[切片模式]slice pattern匹配(不是仅具有单个[休息模式]rest pattern的模式(即 [..]))被视为对切片长度的读取,并通过 ImmBorrow 捕获切片。
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // Captures `*x` by `ImmBorrow`.
&mut [] => (),
// ^^
// This matches a slice of exactly zero elements. To know whether the
// scrutinee matches, the length must be read, causing the slice to
// be captured.
_ => (),
};
let _ = &mut *x; // ERROR: Cannot borrow `*x` as mutable.
c();
}
#![allow(unused)]
fn main() {
let x: &mut [u8] = &mut [];
let c = || match x { // Does not capture `*x`.
[..] => (),
// ^^ Rest pattern.
};
let _ = &mut *x; // OK: `*x` can be borrow here.
c();
}
Note
也许令人惊讶的是,即使长度包含在(宽)指针中,被读取和被捕获的是被指向者(切片)的位置。
#![allow(unused)] fn main() { fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { // The closure outlives `'l` because it captures `**x`. If // instead it captured `*x`, it would not live long enough // to satisfy the `impl Fn() + 'l` bound. || match *x { // Captures `**x` by `ImmBorrow`. &[] => (), _ => (), } } }通过这种方式,行为与在审查者中解引用到切片是一致的。
#![allow(unused)] fn main() { fn f<'l: 's, 's>(x: &'s mut &'l [u8]) -> impl Fn() + 'l { || match **x { // Captures `**x` by `ImmBorrow`. [] => (), _ => (), } } }有关详细信息,请参阅 Rust PR #138961。
由于数组的长度由其类型固定,将数组与切片模式匹配本身不会捕获该位置。
#![allow(unused)]
fn main() {
let x: [u8; 1] = [0];
let c = || match x { // Does not capture `x`.
[_] => (), // Length is fixed.
};
x; // OK: `x` can be moved here.
c();
}
在移动上下文中捕获引用
因为不允许将字段移出引用,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 trait 实现与其他 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:
- 不从任何捕获的变量移出的闭包实现
FnMut,表示它可以通过可变引用调用。
- 不修改或从任何捕获的变量移出的闭包实现
Fn,表示它可以通过共享引用调用。
非捕获闭包是不从其环境捕获任何内容的闭包。非异步、非捕获闭包可以被强制转换为具有匹配签名的函数指针(例如 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);
}
异步闭包 trait
异步闭包返回的 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` } }
异步闭包实现 AsyncFn、AsyncFnMut 和 AsyncFnOnce 的方式类似于常规闭包实现 Fn、FnMut 和 FnOnce;也就是说,取决于其主体中捕获变量的使用。
其他 trait
所有闭包类型都实现 Sized。此外,如果其存储的捕获类型允许,闭包类型还实现以下 trait:
Send 和 Sync 的规则与普通结构体类型的规则匹配,而 Clone 和 Copy 的行为就像派生的一样。对于 Clone,捕获值的克隆顺序未指定。
因为捕获通常是通过引用进行的,所以会出现以下一般规则:
- 如果所有捕获的值都是
Sync,则闭包是Sync的。 - 如果所有通过非唯一不可变引用捕获的值都是
Sync,并且所有通过唯一不可变或可变引用、复制或移动捕获的值都是Send,则闭包是Send的。 - 如果它不通过唯一不可变或可变引用捕获任何值,并且如果它通过复制或移动捕获的所有值分别是
Clone或Copy,则闭包是Clone或Copy的。
丢弃顺序
如果闭包按值捕获复合类型(如结构体、元组和枚举)的字段,则该字段的生命周期现在将绑定到闭包。因此,复合类型的不相交字段可能在不同时间被丢弃。
#![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 关键字通常用于允许闭包比捕获的值活得更久,例如如果闭包被返回或用于生成新线程。
无论数据是否会被闭包读取,即在通配符模式的情况下,如果在闭包内提到在闭包外部定义的变量,则该变量将被完整捕获。
丢弃顺序差异
由于复合类型被完整捕获,按值捕获这些复合类型之一的闭包将在闭包被丢弃时同时丢弃整个捕获的变量。
#![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 ------------+
}
}