RPIT 生命周期捕获规则
本章描述了与 RFC 3498 中引入的 2024 生命周期捕获规则 相关的更改,包括如何使用**不透明类型精确捕获**(在 RFC 3617 中引入的)来迁移你的代码。
总结
- 在 Rust 2024 中,当
use<..>
边界不存在时,所有作用域内的泛型参数,包括生命周期参数,都会被隐式捕获。 Captures
技巧(Captures<..>
边界)和 outlives 技巧(例如'_
边界)的使用可以用use<..>
边界替换(在所有版本中),或者在 Rust 2024 中完全移除。
细节
捕获
在 RPIT(返回位置 impl Trait)不透明类型中捕获泛型参数,允许该参数在相应的隐藏类型中使用。在 Rust 1.82 中,我们添加了 use<..>
边界,允许显式指定要捕获哪些泛型参数。这些将有助于将你的代码迁移到 Rust 2024,并且在本章中将有助于解释版本特定的隐式捕获规则是如何工作的。这些 use<..>
边界看起来像这样:
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn capture<'a, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> { // ~~~~~~~~~~~~~~~~~~~~~~~ // This is the RPIT opaque type. // // It captures `'a` and `T`. (x, y) //~~~~~~ // The hidden type is: `(&'a (), T)`. // // This type can use `'a` and `T` because they were captured. } }
被捕获的泛型参数会影响不透明类型的使用方式。例如,这是一个错误,因为尽管隐藏类型没有使用生命周期,但生命周期仍然被捕获。
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn capture<'a>(_: &'a ()) -> impl Sized + use<'a> {} fn test<'a>(x: &'a ()) -> impl Sized + 'static { capture(x) //~^ ERROR lifetime may not live long enough } }
相反,这是可以的
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn capture<'a>(_: &'a ()) -> impl Sized + use<> {} fn test<'a>(x: &'a ()) -> impl Sized + 'static { capture(x) //~ OK } }
当 use<..>
边界不存在时的版本特定规则
如果 use<..>
边界不存在,那么编译器会使用版本特定的规则来决定隐式捕获哪些作用域内的泛型参数。
在所有版本中,当 use<..>
边界不存在时,所有作用域内的类型和 const 泛型参数都会被隐式捕获。例如:
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn f_implicit<T, const C: usize>() -> impl Sized {} // ~~~~~~~~~~ // No `use<..>` bound is present here. // // In all editions, the above is equivalent to: fn f_explicit<T, const C: usize>() -> impl Sized + use<T, C> {} }
在 Rust 2021 及更早版本中,当 use<..>
边界不存在时,泛型生命周期参数仅当它们在裸函数和固有 impl 中的关联函数及方法的签名中的 RPIT 不透明类型的边界内以语法形式出现时才会被捕获。然而,从 Rust 2024 开始,这些作用域内的泛型生命周期参数会被无条件捕获。例如:
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn f_implicit(_: &()) -> impl Sized {} // In Rust 2021 and earlier, the above is equivalent to: fn f_2021(_: &()) -> impl Sized + use<> {} // In Rust 2024 and later, it's equivalent to: fn f_2024(_: &()) -> impl Sized + use<'_> {} }
这使得行为与 trait impl 中关联函数和方法的签名中的 RPIT 不透明类型、trait 定义中 RPIT 的使用 (RPITIT) 以及由 async fn
创建的不透明 Future
类型保持一致,所有这些类型在所有版本中,当 use<..>
边界不存在时,都会隐式捕获所有作用域内的泛型生命周期参数。
外部泛型参数
当决定隐式捕获什么时,来自外部 impl 的泛型参数被认为在作用域内。例如:
#![allow(unused)] fn main() { #![feature(precise_capturing)] struct S<T, const C: usize>((T, [(); C])); impl<T, const C: usize> S<T, C> { // ~~~~~~~~~~~~~~~~~ // These generic parameters are in scope. fn f_implicit<U>() -> impl Sized {} // ~ ~~~~~~~~~~ // ^ This generic is in scope too. // ^ // | // No `use<..>` bound is present here. // // In all editions, it's equivalent to: fn f_explicit<U>() -> impl Sized + use<T, U, C> {} } }
来自高阶绑定的生命周期
类似地,由高阶 for<..>
绑定引入到作用域中的泛型生命周期参数被认为在作用域内。例如:
#![allow(unused)] fn main() { #![feature(precise_capturing)] trait Tr<'a> { type Ty; } impl Tr<'_> for () { type Ty = (); } fn f_implicit() -> impl for<'a> Tr<'a, Ty = impl Copy> {} // In Rust 2021 and earlier, the above is equivalent to: fn f_2021() -> impl for<'a> Tr<'a, Ty = impl Copy + use<>> {} // In Rust 2024 and later, it's equivalent to: //fn f_2024() -> impl for<'a> Tr<'a, Ty = impl Copy + use<'a>> {} // ~~~~~~~~~~~~~~~~~~~~ // However, note that the capturing of higher-ranked lifetimes in // nested opaque types is not yet supported. }
参数位置 impl Trait (APIT)
由 APIT(参数位置 impl Trait)的使用创建的匿名(即未命名)泛型参数被认为在作用域内。例如:
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn f_implicit(_: impl Sized) -> impl Sized {} // ~~~~~~~~~~ // This is called APIT. // // The above is *roughly* equivalent to: fn f_explicit<_0: Sized>(_: _0) -> impl Sized + use<_0> {} }
请注意,前者并不*完全*等同于后者,因为通过命名泛型参数,现在可以使用 turbofish 语法为其提供参数。除了将其转换为命名泛型参数外,没有其他方法可以在 use<..>
边界中显式包含匿名泛型参数。
迁移
迁移时避免过度捕获
impl_trait_overcaptures
lint 标记了将在 Rust 2024 中捕获额外生命周期的 RPIT 不透明类型。此 lint 是 rust-2024-compatibility
lint 组的一部分,该 lint 组在运行 cargo fix --edition
时会自动应用。在大多数情况下,lint 可以自动插入所需的 use<..>
边界,以便在 Rust 2024 中不会捕获额外的生命周期。
要迁移你的代码以兼容 Rust 2024,请运行
cargo fix --edition
例如,这将改变
#![allow(unused)] fn main() { fn f<'a>(x: &'a ()) -> impl Sized { *x } }
...变成
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn f<'a>(x: &'a ()) -> impl Sized + use<> { *x } }
如果没有这个 use<>
边界,在 Rust 2024 中,不透明类型将捕获 'a
生命周期参数。通过添加此边界,迁移 lint 保留了现有的语义。
迁移涉及 APIT 的情况
在某些情况下,lint 无法自动进行更改,因为需要为泛型参数命名,以便它可以出现在 use<..>
边界内。在这些情况下,lint 会警告你可能需要手动进行更改。例如,给定:
#![allow(unused)] fn main() { fn f<'a>(x: &'a (), y: impl Sized) -> impl Sized { (*x, y) } // ^^ ~~~~~~~~~~ // This is a use of APIT. // //~^ WARN `impl Sized` will capture more lifetimes than possibly intended in edition 2024 //~| NOTE specifically, this lifetime is in scope but not mentioned in the type's bounds fn test<'a>(x: &'a (), y: ()) -> impl Sized + 'static { f(x, y) } }
由于使用了 APIT 以及泛型类型参数必须在 use<..>
边界中命名,因此代码无法自动转换。要将此代码转换为 Rust 2024 而不捕获生命周期,你必须命名该类型参数。例如:
#![allow(unused)] fn main() { #![feature(precise_capturing)] #![deny(impl_trait_overcaptures)] fn f<'a, T: Sized>(x: &'a (), y: T) -> impl Sized + use<T> { (*x, y) } // ~~~~~~~~ // The type parameter has been named here. fn test<'a>(x: &'a (), y: ()) -> impl Sized + use<> { f(x, y) } }
请注意,这稍微更改了函数的 API,因为现在可以使用 turbofish 语法为此参数显式提供类型参数。如果不希望这样做,你可以考虑是否可以继续省略 use<..>
边界并允许捕获生命周期。如果你将来可能想在隐藏类型中使用该生命周期并希望为此节省空间,这可能尤其可取。
从 Captures
技巧迁移
在 Rust 1.82 中引入精确捕获 use<..>
边界之前,在 RPIT 不透明类型中正确捕获生命周期通常需要使用 Captures
技巧。例如:
#![allow(unused)] fn main() { #[doc(hidden)] pub trait Captures<T: ?Sized> {} impl<T: ?Sized, U: ?Sized> Captures<T> for U {} fn f<'a, T>(x: &'a (), y: T) -> impl Sized + Captures<(&'a (), T)> { // ~~~~~~~~~~~~~~~~~~~~~ // This is called the `Captures` trick. (x, y) } fn test<'t, 'x>(t: &'t (), x: &'x ()) { f(t, x); } }
使用 use<..>
边界语法,Captures
技巧不再需要,可以在所有版本中用以下内容替换。
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn f<'a, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> { (x, y) } fn test<'t, 'x>(t: &'t (), x: &'x ()) { f(t, x); } }
在 Rust 2024 中,use<..>
边界通常可以完全省略,上述代码可以简单地写成:
#![allow(unused)] fn main() { #![feature(lifetime_capture_rules_2024)] fn f<'a, T>(x: &'a (), y: T) -> impl Sized { (x, y) } fn test<'t, 'x>(t: &'t (), x: &'x ()) { f(t, x); } }
对此没有自动迁移,并且 Captures
技巧在 Rust 2024 中仍然有效,但你可能需要考虑手动迁移代码以远离使用这个旧技巧。
从 outlives 技巧迁移
在 Rust 1.82 中引入精确捕获 use<..>
边界之前,当需要在某些不透明类型的隐藏类型中使用生命周期时,通常使用 “outlives 技巧”。例如:
#![allow(unused)] fn main() { fn f<'a, T: 'a>(x: &'a (), y: T) -> impl Sized + 'a { // ~~~~ ~~~~ // ^ This is the outlives trick. // | // This bound is needed only for the trick. (x, y) // ~~~~~~ // The hidden type is `(&'a (), T)`. } }
这个技巧不如 Captures
技巧那么繁琐,但也不太正确。正如我们在上面的例子中看到的,即使 T
中的任何生命周期组件都独立于生命周期 'a
,我们也需要添加 T: 'a
边界才能使技巧生效。这给调用者带来了不必要的和令人惊讶的限制。
使用精确捕获,你可以在所有版本中将上述代码替换为:
#![allow(unused)] fn main() { #![feature(precise_capturing)] fn f<T>(x: &(), y: T) -> impl Sized + use<'_, T> { (x, y) } fn test<'t, 'x>(t: &'t (), x: &'x ()) { f(t, x); } }
在 Rust 2024 中,use<..>
边界通常可以完全省略,上述代码可以简单地写成:
#![allow(unused)] fn main() { #![feature(precise_capturing)] #![feature(lifetime_capture_rules_2024)] fn f<T>(x: &(), y: T) -> impl Sized { (x, y) } fn test<'t, 'x>(t: &'t (), x: &'x ()) { f(t, x); } }
对此没有自动迁移,并且 outlives 技巧在 Rust 2024 中仍然有效,但你可能需要考虑手动迁移代码以远离使用这个旧技巧。