随着 Rust 1.65(计划于 11 月 3 日发布)的到来,泛型关联类型 (GATs) 将稳定下来——距离最初的 RFC 开启已超过六年半。这是一个真正巨大的成就;然而,就像 Rust 其他一些重大特性(如 async
或 const 泛型)一样,初步稳定版本中存在一些限制,我们计划在未来移除这些限制。
本文的目的不是教授 GATs 的用法,而是向可能不知道它们是什么的读者简要介绍它们,并列举用户最可能遇到的初步稳定版本中的一些限制。更详细的信息可以在 RFC、GATs 倡议仓库、稳定化工作启动时的 之前的博客文章、Nightly 参考手册中的关联项部分,或者 Github 上关于 GATs 的开放问题 中找到。
什么是 GATs
其核心在于,泛型关联类型允许你在*关联类型*上使用*泛型*(类型、生命周期或常量)。请注意,这只是完善了你可以放置泛型的位置:例如,你已经可以在独立类型别名和 Trait 中的函数上使用泛型。现在你也可以在 Trait 中的类型别名上使用泛型(我们称之为关联类型)。下面是一个带有 GAT 的 Trait 示例:
大部分内容应该看起来很熟悉;这个 Trait 与标准库中的 Iterator
Trait 非常相似。从根本上讲,这个版本的 Trait 允许 next
函数返回一个*借用*自 self
的项。有关示例的更多细节,以及 where Self: 'a
的作用信息,请参阅推动稳定化的文章。
总的来说,GATs 为各种模式和 API 提供了基础。如果你想真正了解有多少项目一直因 GATs 稳定而受阻,可以滚动查看 跟踪问题:你会发现多年来许多其他项目链接到这些讨论串,并说类似“我们希望 API 看起来像 X,但为此我们需要 GATs”的话(或参阅这条评论,它已经汇总了一些)。如果你对 GATs 如何使库实现零拷贝解析,从而性能提升近十倍感兴趣,你可能会对 Niko Matsakis 关于此的博客文章感兴趣。
总而言之,即使*你*不需要直接使用 GATs,你使用的*库*很可能为了易用性、性能或者仅仅因为那是唯一的实现方式而在内部或公开地使用 GATs。
GATs 出错时 - 当前的一些 Bug 和限制
如前所述,这次稳定并非没有 Bug 和限制。这与之前的重大语言特性相比并非异常。我们计划作为新成立的类型团队推动的持续努力的一部分,修复这些 Bug 并移除这些限制。(敬请期待即将发布的官方公告中的更多细节!)
在这里,我们将介绍一些我们已经确定用户可能会遇到的限制。
'static
要求
高阶 Trait 约束隐含的 考虑以下代码:
在这里,想象我们想有一个 LendingIterator
,其项是数组中重叠的切片。我们还有一个 print_items
函数,它打印 LendingIterator
的每个项,只要它们实现了 Debug
。这一切看起来都足够简单,但上述代码无法编译——尽管它应该可以。在这里不深入细节,for<'a> I::Item<'a>: Debug
目前意味着 I::Item<'a>
必须拥有 'static
的生命周期。
这不是一个好的 Bug。在今天我们将提到的所有 Bug 中,这很可能是最具限制性、最烦人且最难理解的一个。它在使用 GATs 时更常出现,但即使是不使用 GATs 的代码也可能遇到。不幸的是,修复这个问题需要对编译器进行一些重构,这不是一个短期项目。不过,它已经在日程上了。好消息是,与此同时,我们正在努力改进从这段代码获得的错误消息。这是它在即将到来的稳定版本中的样子:
error: `array` does not live long enough
|
| let slice = &mut array;
;
;
}
| - `array` dropped here while still borrowed
|
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
| for<'a> Item: Debug,
它不完美,但总比没有好。它可能无法涵盖所有情况,但如果你在某个地方有 for<'a> I::Item<'a>: Trait
约束并收到一个错误,说某个东西生命周期不够长,你可能遇到了这个 Bug。我们正在积极努力修复此问题。然而,根据我们的经验,这个错误实际上并不像你阅读本文时想象的那样经常出现,因此我们觉得即使存在这个问题,这个特性仍然非常有用。
带有 GATs 的 Trait 不是 Object Safe
好吧,这是一个简单的问题。使带有 GATs 的 Trait Object Safe 需要一些设计工作来实现。为了了解这里还需要做的工作,让我们先看看一段你今天就可以在稳定版上编写的代码:
好吧,你可以写这段代码,但它无法编译:
error: the value of the associated type `Item` must be specified
-/lib.rs:1:23
|
1 |
| ^^^^^^^^ help: specify the associated type: ` `
对于一个格式正确的 Trait Object,它必须为所有关联类型指定一个值。出于同样的原因,我们不想接受以下代码:
然而,GATs 引入了一些额外的复杂性。看这段代码:
因此,我们为 Item 生命周期的某个特定值 ('static
) 指定了关联类型的值,但不是针对*任何*值,像这样:
尽管我们对如何在将来的一些 Trait Solver 迭代(它使用更逻辑化的公式)中实现此要求有了清晰的思路,但在当前的 Trait Solver 中实现它更加困难。因此,我们选择暂时搁置此问题。
借用检查器并不完美,这一点很明显
沿用 LendingIterator
的例子,让我们先看看 Iterator
的两个方法:for_each
和 filter
:
这两个方法都接受一个函数作为参数。通常使用闭包。现在,让我们看看 LendingIterator
的定义:
看起来很简单,但如果真的那么简单,还会出现在这里吗?让我们先看看尝试使用 for_each
时会发生什么:
error: `I` does not live long enough
|
| iter.for_each
| ^^^^^^^^^^
嗯,这不太好。事实证明,这与我们之前讨论的第一个限制密切相关,即使借用检查器在这里确实起作用。
另一方面,让我们看看一个非常明显的借用检查器问题,查看由 filter
方法返回的 Filter
Struct 的实现:
同样,这里的实现应该不会令人惊讶。我们理所当然地遇到了一个借用检查器错误:
error: cannot borrow `self.iter` as mutable more than once at a time
-/main.rs:28:32
|
27 |
这是当前借用检查器中的已知限制,应该在将来的一些迭代(如 Polonius)中解决。
where
子句的非局部要求
GATs 的 今天我们要讨论的最后一个限制有些不同;它不是一个 Bug,也不应该阻止任何程序编译。但这一切都回到了你在这篇文章中多次看到的 where Self: 'a
子句。如前所述,如果你有兴趣深入了解为什么需要那个子句,请参阅推动稳定化的文章。
关于这个子句,有一个不太理想的要求:你必须在 Trait 上写它。和函数上的 where
子句一样,你不能在 Trait 中没有的 impl
中为关联类型添加子句。然而,如果你不添加这个子句,该 Trait 的大量潜在实现将被禁止。
为了帮助用户避免不小心忘记添加这个子句(或具有相同效果的类似子句,对于不同的泛型集合)的陷阱,我们实现了一套必须遵循的规则,以便带有 GATs 的 Trait 能够编译。让我们先看看不写子句时的错误:
error: missing required bound on `Item`
-/lib.rs:2:5
|
2 | type Item<'a>;
| help: add the required where clause: `where Self: 'a`
|
= note: this bound is currently required to ensure that impls have maximum flexibility
= note: we are soliciting feedback, see issue #87479 <https://github.com/rust-lang/rust/issues/87479> for more information
希望这个错误能有所帮助(你甚至可以使用 cargo fix
来修复它!)。但是,这些规则到底是什么?嗯,最终,它们变得有点简单:*对于使用 GAT 的方法,任何可以被证明的约束也必须存在于 GAT 本身。*
好的,那么我们是如何得出必需的 Self: 'a
约束的呢?嗯,让我们看看 next
方法。它返回 Self::Item<'a>
,并且我们有一个参数 &'a mut self
。我们有点深入到 Rust 语言的细节中,但由于该参数,我们知道 Self: 'a
必须成立。因此,我们需要那个约束。
我们现在要求这些约束是为了将来可能自动隐含它们留出空间(当然也是因为这应该有助于用户编写带有 GATs 的 Trait)。它们不应该干扰任何实际的使用场景,但如果你确实遇到了问题,查看上面错误中提到的问题。如果你想看到不同场景下(关于何时需要哪些约束)相当全面的测试,查看相关的测试文件。
结论
希望这里提出的限制及其解释不会削弱对 GATs 稳定化的总体兴奋感。诚然,这些限制确实...限制了你能用 GATs 做的事情的数量。*然而*,如果我们不认为 GATs 仍然*非常有用*,我们就不会稳定化 GATs。此外,如果我们不认为这些限制是无法解决的(并且以向后兼容的方式),我们就不会稳定化 GATs。
最后,促成这次稳定化的所有相关人员都值得最诚挚的感谢。如前所述,它已经经历了六年半,没有每个人的支持和奉献,这一切都不可能发生。感谢所有人!