闭包中的不相交捕获
概要
|| a.x + 1
现在只捕获a.x
,而不是a
。- 这会导致在不同的时间丢弃数据,或者影响闭包是否实现诸如
Send
或Clone
之类的 trait。- 如果检测到可能的更改,
cargo fix
将插入诸如let _ = &a
之类的语句,以强制闭包捕获整个变量。
- 如果检测到可能的更改,
详细信息
闭包会自动捕获您在其主体中引用的任何内容。例如,|| a + 1
会自动捕获对周围上下文中 a
的引用。
在 Rust 2018 及更早的版本中,闭包捕获整个变量,即使闭包只使用一个字段。例如,|| a.x + 1
捕获对 a
的引用,而不仅仅是 a.x
。完整捕获 a
会阻止对 a
的其他字段进行修改或移动,因此像这样的代码无法编译
let a = SomeStruct::new();
drop(a.x); // Move out of one field of the struct
println!("{}", a.y); // Ok: Still use another field of the struct
let c = || println!("{}", a.y); // Error: Tries to capture all of `a`
c();
从 Rust 2021 开始,闭包捕获更加精确。通常,它们只会捕获它们使用的字段(在某些情况下,它们可能会捕获超出它们使用的内容,有关完整详细信息,请参阅 Rust 参考)。因此,上面的示例在 Rust 2021 中可以正常编译。
不相交捕获是作为 RFC 2229 的一部分提出的,RFC 包含有关动机的详细信息。
迁移
作为 2021 版本的一部分,添加了一个迁移 lint,rust_2021_incompatible_closure_captures
,以帮助将 Rust 2018 代码库自动迁移到 Rust 2021。
为了将您的代码迁移为与 Rust 2021 版本兼容,请运行
cargo fix --edition
下面是如何手动迁移代码以使用与 Rust 2021 兼容的闭包捕获的示例,如果自动迁移失败或者您想更好地了解迁移的工作原理。
更改闭包捕获的变量可能会导致程序更改行为或在两种情况下停止编译
每当检测到以下任何场景时,cargo fix
将在您的闭包中插入一个“虚拟 let”,以强制其捕获整个变量
#![allow(unused)] fn main() { let x = (vec![22], vec![23]); let c = move || { // "Dummy let" that forces `x` to be captured in its entirety let _ = &x; // Otherwise, only `x.0` would be captured here println!("{:?}", x.0); }; }
这是一个保守的分析:在许多情况下,可以安全地删除这些虚拟 let,并且您的程序将正常工作。
通配符模式
闭包现在只捕获需要读取的数据,这意味着以下闭包不会捕获 x
#![allow(unused)] fn main() { let x = 10; let c = || { let _ = x; // no-op }; let c = || match x { _ => println!("Hello World!") }; }
这里的 let _ = x
语句是一个空操作,因为 _
模式完全忽略了右侧,而 x
是对内存中位置的引用(在本例中,是一个变量)。
此更改本身(捕获更少的值)不会触发任何建议,但它可能会与下面的“丢弃顺序”更改一起触发。
微妙之处: 还有其他类似的表达式,例如我们插入的“虚拟 let”let _ = &x
,它们不是空操作。这是因为右侧 (&x
) 不是对内存中位置的引用,而是一个必须首先评估的表达式(然后丢弃其结果)。
丢弃顺序
当闭包从变量 t
中获取一个值的所有权时,该值会在闭包被丢弃时被丢弃,而不是在变量 t
超出作用域时被丢弃
#![allow(unused)] fn main() { fn move_value<T>(_: T){} { let t = (vec![0], vec![0]); { let c = || move_value(t); // t is moved here } // c is dropped, which drops the tuple `t` as well } // t goes out of scope here }
以上代码在 Rust 2018 和 Rust 2021 中都将运行相同。但是,在闭包仅拥有变量部分所有权的情况下,可能会存在差异
#![allow(unused)] fn main() { fn move_value<T>(_: T){} { let t = (vec![0], vec![0]); { let c = || { // In Rust 2018, captures all of `t`. // In Rust 2021, captures only `t.0` move_value(t.0); }; // In Rust 2018, `c` (and `t`) are both dropped when we // exit this block. // // In Rust 2021, `c` and `t.0` are both dropped when we // exit this block. } // In Rust 2018, the value from `t` has been moved and is // not dropped. // // In Rust 2021, the value from `t.0` has been moved, but `t.1` // remains, so it will be dropped here. } }
在大多数情况下,在不同时间丢弃值只会影响何时释放内存,这并不重要。但是,某些 Drop
impls(也称为析构函数)具有副作用,并且在这种情况下更改丢弃顺序可能会改变程序的语义。在这种情况下,编译器将建议插入一个虚拟 let
以强制捕获整个变量。
Trait 实现
闭包会根据它们捕获的值自动实现以下 trait
Clone
:如果所有捕获的值都是Clone
。- 自动 trait,例如
Send
、Sync
和UnwindSafe
:如果所有捕获的值都实现了给定的 trait。
在 Rust 2021 中,由于捕获的值不同,这会影响闭包将实现的 trait。迁移 lint 会测试每个闭包,以查看它之前是否已经实现了给定的 trait,以及现在是否仍然实现它;如果他们发现曾经实现过的 trait 不再实现,则会插入“虚拟 let”。
例如,在线程之间传递原始指针的常用方法是将它们包装在一个结构体中,然后为该包装器实现 Send
/Sync
自动 trait。传递给 thread::spawn
的闭包使用包装器内的特定字段,但无论如何都会捕获整个包装器。由于包装器是 Send
/Sync
,因此该代码被认为是安全的,因此可以成功编译。
使用不相交捕获,只会捕获闭包中提到的特定字段,而该字段最初不是 Send
/Sync
,从而破坏了包装器的目的。
#![allow(unused)] fn main() { use std::thread; struct Ptr(*mut i32); unsafe impl Send for Ptr {} let mut x = 5; let px = Ptr(&mut x as *mut i32); let c = thread::spawn(move || { unsafe { *(px.0) += 10; } }); // Closure captured px.0 which is not Send }