Rust 1.65 中稳定泛型关联类型

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

从 Rust 1.65 开始,泛型关联类型 (GAT) 将稳定下来,这距离最初的 RFC 开启已经超过六年半了。这确实是一项重大的成就;然而,与 Rust 的其他一些重大功能(如 async 或 const 泛型)一样,在最初的稳定化中存在一些限制,我们计划在将来消除这些限制。

本文的目标不是讲解 GAT,而是向可能不了解 GAT 的读者简要介绍它们,并列举用户最有可能遇到的初始稳定化中的一些限制。更详细的信息可以在 RFCGATs initiative repository、稳定化推动开始时的 博客文章夜间版参考中的关联项部分GitHub 上有关 GAT 的开放问题 中找到。

什么是 GAT

从本质上讲,泛型关联类型允许你在关联类型上使用泛型(类型、生命周期或常量)。请注意,这实际上只是完善了你可以放置泛型的所有位置:例如,你已经可以在独立的类型别名和特质中的函数上使用泛型。现在你可以在特质中的类型别名(我们称之为关联类型)上使用泛型。以下是一个带有 GAT 的特质示例

trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Self::Item<'a>;
}

其中大部分应该看起来很熟悉;这个特质看起来非常类似于标准库中的 Iterator 特质。从根本上说,这个版本的特质允许 next 函数返回一个借用self 的项。有关示例的更多详细信息,以及有关 where Self: 'a 的一些信息,请查看 稳定化推动文章

总的来说,GAT 为各种模式和 API 提供了基础。如果你真的想了解有多少项目因为 GAT 稳定而被阻塞,请滚动浏览 跟踪问题:你会发现多年来来自其他项目的许多问题链接到这些线程,内容类似于“我们希望 API 看起来像 X,但为此我们需要 GAT”(或者查看 此评论,其中已经整理了一些)。如果你对 GAT 如何使库能够进行零拷贝解析,从而导致性能提高近十倍感兴趣,你可能想查看 Niko Matsakis 的 博客文章

总而言之,即使不需要直接使用 GAT,你使用的也可能在内部或公开使用 GAT,以提高人体工程学、性能,或者仅仅因为这是实现的唯一方式。

当 GAT 出错时 - 一些当前的错误和限制

如前所述,这种稳定化并非没有错误和限制。与之前的大型语言功能相比,这并不罕见。我们计划将这些错误修复并消除这些限制,作为新成立的类型团队推动的持续工作的一部分。(敬请期待即将发布的官方公告中了解更多详细信息!)

在这里,我们将介绍几个我们已经发现的用户可能会遇到的限制。

来自更高阶特质边界隐含的 '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,它打印 LendingIterator 的每个项,只要它们实现了 Debug。这看起来都足够无辜,但上面的代码无法编译 - 即使它应该可以。不在这里详细说明,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 的特质不是对象安全的

所以,这个很简单。使带有 GAT 的特质成为对象安全的将需要一些设计工作来实现。为了了解这里还有多少工作要做,让我们从一段你今天可以在稳定版上编写的代码开始

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>`

为了使特质对象格式良好,它必须为所有关联类型指定一个值。出于同样的原因,我们不想接受以下内容

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>) {}

虽然我们对如何在未来的某些迭代中实现特质求解器(使用更多逻辑公式)中的要求有一个明确的想法,但在当前的特质求解器中实现它更困难。因此,我们选择暂时搁置这个问题。

借用检查器并不完美,它表现出来了

继续使用 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 子句。如前所述,如果你想深入了解为什么需要这个子句,请查看 稳定化推动文章

关于这个子句有一个不太理想的要求:你必须在特质上编写它。就像函数上的 where 子句一样,你不能在实现中添加不在特质中的关联类型的子句。但是,如果你没有添加这个子句,一大组潜在的特质实现将被禁止。

为了帮助用户不要掉入意外忘记添加这个(或类似的子句,最终对不同的泛型集产生相同的效果)的陷阱,我们实现了一组必须遵循的规则,才能使带有 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 的特质)。它们不应该干扰任何实际用例,但如果你遇到问题,请查看上面错误中提到的问题。如果你想查看关于哪些边界是必需的以及何时必需的各种场景的相当全面的测试,请查看相关的 测试文件

结论

希望这里提到的限制及其解释不会削弱 GAT 稳定带来的整体兴奋感。当然,这些限制确实会限制你可以用 GAT 做的事情。然而,如果我们不认为 GAT 仍然非常有用,我们就不会稳定 GAT。此外,如果我们不认为这些限制是可以解决的(并且以向后兼容的方式),我们也不会稳定 GAT。

最后,所有参与 GAT 稳定的人员都应得到最大的感谢。正如之前所说,这已经酝酿了 6.5 年,如果没有大家的支持和奉献,这是不可能实现的。感谢大家!