这篇博文讨论了 Rust 中 impl Trait 特性的重大变化,这些变化将在 Rust 2024 中生效。 主要重点是修改通用参数在返回位置 impl Trait 中的使用规则,旨在提高可用性和灵活性。
默认行为:
默认行为现在允许返回位置植入 Trait 的隐藏类型使用作用域中的任何通用参数,包括生命周期。 这与以前的限制形成了鲜明对比,以前除非明确说明,否则只自动允许使用类型参数。
使用Bound 绑定语法:
将引入一种称为 "use Bound "的新语法,使开发人员能够指定隐藏类型中可以使用的类型和生命周期。 例如, impl Trait + use<'x, T> 表示隐藏类型可以使用生命周期 'x 和类型 T,但不包括其他通用参数。
fn process_data( |
这里在返回位置使用 -> impl Iterator 表示函数返回 "某种迭代器Iterator"。
实际类型将由编译器根据函数体确定。
之所以称其为 "隐藏类型",是因为调用者并不知道它的确切类型;他们必须根据迭代器trait进行编码。不过,在代码生成时,编译器会根据实际的精确类型生成代码,从而确保调用者得到完全优化。
虽然调用者不知道确切的类型,但他们确实需要知道函数将继续借用数据参数,这样才能确保数据引用在迭代过程中保持有效。
此外,调用者必须能够仅根据类型签名而不查看函数体来确定这一点。
Rust 目前的规则是,只有当引用的生命周期出现在 impl Trait 本身中时,返回位置的 impl Trait 值才能使用引用。
在本例中,impl Iterator
在这种情况下得到的错误信息("隐藏类型捕获生命周期")并不是最直观的,但它提供了一个有用的修复建议:help: to declare that
<code>impl Iterator<Item = ProcessedDatum></code>
captures <code>'_</code>, you can add an
explicit <code>'_</code> lifetime bound
|
5 | ) -> impl Iterator<Item = ProcessedDatum> + '_ {
| ++++
根据这一建议,函数签名变得稍微明确一些:下面代码多了函数参数d
fn process_data<'d>( |
在当前新版本中,数据的生命周期 'd 在 impl Trait 类型中被明确引用,因此允许使用。 这也是向调用者发出的一个信号,即只要迭代器在使用中,数据的借用就必须持续,这意味着在这样的示例中,它会(正确地)提示错误:
let mut data: Vec<Datum> = vec![Datum::default()]; |
impl Trait设计问题
impl Trait早期基于一组有限的示例确定了在 中使用通用参数的规则。随着时间的推移,我们注意到了其中存在的许多问题。
- 不是正确的默认设置
- 不够灵活
- 很难解释
- 错误建议可能会让人困惑
- 与 Rust 其他部分不一致
我们对 crates.io 上的库包进行了调查,发现绝大多数涉及返回位置 impl 特征和泛型的情况的界限都太强,可能会导致不必要的错误(尽管它们通常以简单的方式使用,不会触发错误)。
必须更改规则
才能允许隐藏类型统一使用所有泛型参数(类型和生命周期)。
Rust 2024 设计
- 新的默认设置,即返回位置植入 Trait 的隐藏类型可以使用作用域中的任何泛型参数,而不仅仅是类型(仅适用于 Rust 2024);
- 用于明确声明可以使用哪些类型的语法(可用于任何版本)。
新的显式语法称为 "use bound":
例如, impl Trait + use<'x, T> 表示允许隐藏类型使用'x 和 T(但不允许使用作用域中的任何其他泛型参数)。
默认情况下可以使用生命周期
在 Rust 2024 中,默认情况下返回位置植入 Trait 值的隐藏类型使用作用域中的任何泛型参数,无论是类型还是生命周期。 这意味着本博文的开始的示例现在可以在 Rust 2024 中正常编译:
fn process_data( |
Impl 特质可以包含一个 use<> 绑定,以精确地指定它们使用的通用类型和生命周期
副作用
作为这一变更的副作用,如果您手动将代码移至 Rust 2024(未进行货物修复),那么带有 impl Trait 返回类型的函数的调用者可能会开始出错。
- 这是因为现在假定这些 impl Trait 类型可能会使用输入生命周期,而不仅仅是类型。
- 要控制这一点,可以使用新的 use<> 绑定语法,明确声明隐藏类型可以使用哪些泛型参数。
上述情况的例外情况是,函数接收的引用参数仅用于读取值,不会包含在返回值中。
下面的函数 indices() 就是这样一个例子:它接收了一个&[T]类型的片段,但唯一的作用是读取长度,用来创建一个迭代器。 在返回值中不需要片段本身:
fn indices<'s, T>( |
在 Rust 2021 中,该声明隐含地表示在返回类型中不使用 slice。 但在 Rust 2024 中,默认情况正好相反。 这意味着在 Rust 2024 中,像这样的调用程序将停止编译,因为它们现在假定在迭代完成之前数据是借用的:
fn main() { |
这可能正是你想要的! 这意味着您可以稍后修改 indices() 的定义,使其确实在结果中包含切片。
换句话说,新的默认值延续了 impl Trait 的传统,即在不影响调用者的情况下,为函数的实现保留了灵活性。
但如果这不是你想要的呢? 如果你想保证 indices() 的返回值中不会保留对其参数片的引用,该怎么办?
现在可以在返回类型中加入 use<> 约束,明确说明哪些泛型参数可以包含在返回类型中。
在 indices() 中,返回类型实际上没有使用任何泛型参数,因此我们最好写成 use<>:
fn indices<'s, T>( |
结论
本例说明了新版本可以帮助我们消除 Rust 的复杂性。
在 Rust 2021 中,关于何时可以在植入trait中使用生命周期参数的默认规则并不适用:
- 它们经常无法表达用户所需的内容,导致需要使用晦涩难懂的变通方法。
- 这些规则导致了其他不一致,例如 -> impl Future 和 async fn 之间的不一致,
- 或者顶层函数中返回位置 impl Trait 和 trait 函数之间的语义不一致。
多亏了2024版本,我们才能够在不破坏现有代码的情况下解决这些问题。
有了 Rust 2024 中更新的规则,大多数代码都能在 Rust 2024 中 "正常工作",避免出现令人困惑的错误;对于需要注释的代码,我们现在有了更强大的注释机制,可以让你准确地表达你需要表达的内容。
网友:
1、我一直遵循的原则是,函数接口的输入应尽可能通用,输出应尽可能具体。这一变化似乎鼓励人们背离后者。
2、我们从显式捕获转变为全部捕获,并在需要时选择退出?
3、讨厌这种变化。现在东西都是隐式借用的?“显式优于隐式”的哲学在哪里?
4、生命周期不能省略,必须显式。(因为生命周期类似上下文 界限上下文 限定上下文)
5、这种新impl trait行为是显式的:它显式地在类型签名中包含所有泛型参数,除非存在一个use<>边界,该边界还显式地列出了所包含的参数。