Rust 1.65 中泛型关联类型将稳定

2022 年 10 月 28 日 · Jack Huey 代表 类型团队

截至 Rust 1.65(计划于 11 月 3 日发布),泛型关联类型 (GATs) 将会稳定——自从最初的 RFC 打开以来,已经过去了六年半的时间。这确实是一项具有里程碑意义的成就;然而,就像 Rust 的其他一些具有里程碑意义的功能一样,例如 async 或常量泛型,在初始稳定化中存在一些限制,我们计划在未来消除这些限制。

这篇文章的目的不是教大家关于 GATs 的知识,而是向可能不了解它们是什么的读者简单介绍一下,并列举一些用户最有可能遇到的初始稳定化中的限制。更多详细信息可以在 RFCGATs 倡议存储库、之前在稳定化启动期间发布的 博客文章夜间参考中的关联项部分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 出错时 - 一些当前的错误和限制

如前所述,这种稳定化并非没有错误和限制。这与之前的大型语言功能相比并不少见。我们计划修复这些错误并消除这些限制,这是由新成立的类型团队推动的持续工作的一部分。(请继续关注,稍后会发布官方公告!)

在这里,我们将只介绍我们已识别的用户可能会遇到的一些限制。

来自高阶 trait 边界的隐含 'static 要求

考虑以下代码:

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_eachfilter

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 年,没有大家的支持和奉献精神,这一切是不可能发生的。感谢大家!