截至 Rust 1.65(计划于 11 月 3 日发布),泛型关联类型 (GATs) 将会稳定——自从最初的 RFC 打开以来,已经过去了六年半的时间。这确实是一项具有里程碑意义的成就;然而,就像 Rust 的其他一些具有里程碑意义的功能一样,例如 async
或常量泛型,在初始稳定化中存在一些限制,我们计划在未来消除这些限制。
这篇文章的目的不是教大家关于 GATs 的知识,而是向可能不了解它们是什么的读者简单介绍一下,并列举一些用户最有可能遇到的初始稳定化中的限制。更多详细信息可以在 RFC、GATs 倡议存储库、之前在稳定化启动期间发布的 博客文章、夜间参考中的关联项部分或 Github 上关于 GATs 的未解决问题中找到。
什么是 GATs
从本质上讲,泛型关联类型允许你在关联类型上拥有泛型(类型、生命周期或常量)。请注意,这实际上只是完善了你可以放置泛型的位置:例如,你已经可以在独立的类型别名和 trait 中的函数上使用泛型。现在,你只需在 trait 中的类型别名(我们称之为关联类型)上使用泛型即可。这是一个带有 GAT 的 trait 的示例:
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next<'a>(&'a mut self) -> Self::Item<'a>;
}
大多数内容应该看起来很熟悉;这个 trait 看上去与标准库中的 Iterator
trait 非常相似。从根本上说,此版本的 trait 允许 next
函数返回一个从 self
借用的项。有关该示例的更多详细信息,以及有关 where Self: 'a
用途的一些信息,请查看 稳定化推送文章。
总的来说,GATs 为各种模式和 API 提供了基础。如果你真的想了解有多少项目因为 GATs 未稳定而受阻,请浏览 跟踪问题:你会发现来自其他项目的许多问题链接到这些线程,多年来一直说着类似“我们希望 API 看起来像 X,但为此我们需要 GATs”的话(或查看 此评论,其中已经汇总了一些)。如果你有兴趣了解 GATs 如何使库执行零拷贝解析,从而使性能提高近十倍,你可能会有兴趣查看 Niko Matsakis 的一篇关于此的 博客文章。
总而言之,即使你不需要直接使用 GATs,你使用的库也很可能在内部或公开使用 GATs 来实现人体工程学、性能,或者只是因为这是实现工作的唯一方法。
GATs 出错时 - 一些当前的错误和限制
如前所述,这种稳定化并非没有错误和限制。这与之前的大型语言功能相比并不少见。我们计划修复这些错误并消除这些限制,这是由新成立的类型团队推动的持续工作的一部分。(请继续关注,稍后会发布官方公告!)
在这里,我们将只介绍我们已识别的用户可能会遇到的一些限制。
'static
要求
来自高阶 trait 边界的隐含 考虑以下代码:
trait LendingIterator {
type Item<'a> where Self: 'a;
}
pub struct WindowsMut<'x, T> {
slice: &'x mut [T],
}
impl<'x, T> LendingIterator for WindowsMut<'x, T> {
type Item<'a> = &'a mut [T] where Self: 'a;
}
fn print_items<I>(iter: I)
where
I: LendingIterator,
for<'a> I::Item<'a>: Debug,
{ ... }
fn main() {
let mut array = [0; 16];
let slice = &mut array;
let windows = WindowsMut { slice };
print_items::<WindowsMut<'_, usize>>(windows);
}
在这里,假设我们想要一个 LendingIterator
,其中项是数组的重叠切片。我们还有一个函数 print_items
,只要它们实现了 Debug
,它就会打印 LendingIterator
的每个项。这看起来都很简单,但上面的代码无法编译 - 即使它应该可以编译。在此不做详细介绍,for<'a> I::Item<'a>: Debug
当前意味着 I::Item<'a>
必须比 'static
更长寿。
这确实不是一个好的错误。在我们今天提到的所有错误中,这可能是最具限制性、最令人恼火和最难弄清楚的错误。这种情况在使用 GAT 时会更频繁地出现,但也可以在完全不使用 GAT 的代码中找到。不幸的是,修复此问题需要对编译器进行一些重构,这不是一个短期项目。不过,它即将到来。好消息是,同时,我们正在努力改进你从此代码收到的错误消息。这是它在即将到来的稳定化版本中的样子:
error[E0597]: `array` does not live long enough
|
| let slice = &mut array;
| ^^^^^^^^^^ borrowed value does not live long enough
| let windows = WindowsMut { slice };
| print_items::<WindowsMut<'_, usize>>(windows);
| -------------------------------------------- argument requires that `array` is borrowed for `'static`
| }
| - `array` dropped here while still borrowed
|
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
|
| for<'a> I::Item<'a>: Debug,
| ^^^^
它并不完美,但总比没有好。它可能无法涵盖所有情况,但如果在某处有一个 for<'a> I::Item<'a>: Trait
边界,并且收到一条错误,提示某些内容不够长寿,你可能遇到了这个错误。我们正在积极努力修复此问题。但是,根据我们的经验,这个错误实际上并不像你在阅读本文时预期的那样频繁出现,因此我们认为即使有它,该功能仍然非常有用。
带有 GAT 的 trait 不是对象安全的
所以,这是一个简单的问题。使带有 GAT 的 trait 对象安全需要一些设计工作来实现。要了解这里剩余的工作,让我们从一段你今天可以在稳定版上编写的代码开始:
fn takes_iter(_: &dyn Iterator) {}
好吧,你可以编写这个,但它无法编译:
error[E0191]: the value of the associated type `Item` (from trait `Iterator`) must be specified
--> src/lib.rs:1:23
|
1 | fn takes_iter(_: &dyn Iterator) {}
| ^^^^^^^^ help: specify the associated type: `Iterator<Item = Type>`
为了使 trait 对象格式良好,它必须为所有关联类型指定一个值。出于同样的原因,我们不想接受以下内容:
fn no_associated_type(_: &dyn LendingIterator) {}
但是,GAT 引入了额外的复杂性。请看这段代码:
fn not_fully_generic(_: &dyn LendingIterator<Item<'static> = &'static str>) {}
因此,我们为 Item
的生命周期的一个值('static
)指定了关联类型的值,而不是为任何值指定,如下所示:
fn fully_generic(_: &dyn for<'a> LendingIterator<Item<'a> = &'a str>) {}
虽然我们对如何在 trait 求解器的某些未来迭代中实现需求(使用更多逻辑公式)有一个坚定的想法,但在当前的 trait 求解器中实现它更困难。因此,我们选择暂时搁置这个问题。
借用检查器并不完美,并且会表现出来
继续使用 LendingIterator
示例,让我们首先看一下 Iterator
上的两个方法:for_each
和 filter
:
trait Iterator {
type Item;
fn for_each<F>(self, f: F)
where
Self: Sized,
F: FnMut(Self::Item);
fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
Self: Sized,
P: FnMut(&Self::Item) -> bool;
}
这两个都将函数作为参数。通常会使用闭包。现在,让我们看看 LendingIterator
的定义:
trait LendingIterator {
type Item<'a> where Self: 'a;
fn for_each<F>(mut self, mut f: F)
where
Self: Sized,
F: FnMut(Self::Item<'_>);
fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
Self: Sized,
P: FnMut(&Self::Item<'_>) -> bool;
}
看起来很简单,但如果真的简单,它会出现在这里吗?让我们首先看看当我们尝试使用 for_each
时会发生什么:
fn iterate<T, I: for<'a> LendingIterator<Item<'a> = &'a T>>(iter: I) {
iter.for_each(|_: &T| {})
}
error: `I` does not live long enough
|
| iter.for_each(|_: &T| {})
| ^^^^^^^^^^
嗯,这不太好。事实证明,这与我们之前讨论的第一个限制非常相关,尽管借用检查器在这里确实发挥了作用。
另一方面,让我们通过查看 filter
方法返回的 Filter
结构的实现,来查看一个非常明显的借用检查器问题:
impl<I: LendingIterator, P> LendingIterator for Filter<I, P>
where
P: FnMut(&I::Item<'_>) -> bool, // <- the bound from above, a function
{
type Item<'a> = I::Item<'a> where Self: 'a; // <- Use the underlying type
fn next(&mut self) -> Option<I::Item<'_>> {
// Loop through each item in the underlying `LendingIterator`...
while let Some(item) = self.iter.next() {
// ...check if the predicate holds for the item...
if (self.predicate)(&item) {
// ...and return it if it does
return Some(item);
}
}
// Return `None` when we're out of items
return None;
}
}
同样,这里的实现应该不会让人感到惊讶。当然,我们遇到了借用检查器错误:
error[E0499]: cannot borrow `self.iter` as mutable more than once at a time
--> src/main.rs:28:32
|
27 | fn next(&mut self) -> Option<I::Item<'_>> {
| - let's call the lifetime of this reference `'1`
28 | while let Some(item) = self.iter.next() {
| ^^^^^^^^^^^^^^^^ `self.iter` was mutably borrowed here in the previous iteration of the loop
29 | if (self.predicate)(&item) {
30 | return Some(item);
| ---------- returning this value requires that `self.iter` is borrowed for `'1`
这是当前借用检查器中一个已知的限制,应该在未来的某些迭代中解决(如 Polonius)。
GAT 上 where 子句的非本地要求
我们今天将要讨论的最后一个限制与其他限制有点不同;它不是一个错误,也不应该阻止任何程序编译。但这一切都回到了你在本文的多个部分中看到的 where Self: 'a
子句。如前所述,如果你有兴趣深入研究为什么需要该子句,请参阅 稳定化推送文章。
关于此子句有一个不太理想的要求:你必须将其写在 trait 上。与函数上的 where 子句一样,你不能在 impl 中向关联类型添加 trait 中不存在的子句。但是,如果你没有添加此子句,则会禁止大量潜在的 trait 的 impl。
为了帮助用户避免意外忘记添加此子句(或导致不同泛型的相同效果的类似子句)的陷阱,我们实现了一组规则,trait 必须遵循这些规则才能使用 GAT 编译。让我们首先看看在不编写该子句的情况下发生的错误:
trait LendingIterator {
type Item<'a>;
fn next<'a>(&'a mut self) -> Self::Item<'a>;
}
error: missing required bound on `Item`
--> src/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
必须成立。所以,我们需要这个边界。
我们现在需要这些边界,以便将来可能自动暗示这些边界(当然,因为它应该帮助用户编写带有 GAT 的 trait)。它们不应干扰任何实际的用例,但是如果你确实遇到问题,请查看上面错误中提到的问题。如果你想查看对不同场景的相当全面的测试,了解需要哪些边界以及何时需要,请查看相关的 测试文件。
结论
希望此处提出的限制及其解释不会减损人们对 GATs 稳定化的整体兴奋。当然,这些限制确实会限制你可以使用 GAT 执行的操作的数量。但是,如果我们不认为 GATs 仍然非常有用,我们就不会稳定 GATs。此外,如果我们不认为这些限制是可解决的(并且以向后兼容的方式),我们就不会稳定 GATs。
最后,所有参与此稳定化过程的各种人员都应受到最衷心的感谢。正如之前所说,它已经历了 6.5 年,没有大家的支持和奉献精神,这一切是不可能发生的。感谢大家!