特质(trait)的泛型类型和关联类型之间有什么区别?

11

在Rust中引入通用关联类型之前,已经有人询问过这个问题,尽管它们已被提出并得到开发

我的理解是trait泛型和关联类型之间在可以绑定到一个结构体上的类型数量上存在差异。

泛型可以绑定任意数量的类型:

struct Struct;

trait Generic<G> {
    fn generic(&self, generic: G);
}

impl<G> Generic<G> for Struct {
    fn generic(&self, _: G) {}
}

fn main() {
    Struct.generic(1);
    Struct.generic("a");
}

关联类型 与1个类型绑定:

struct Struct;

trait Associated {
    type Associated;

    fn associated(&self, associated: Self::Associated);
}

impl Associated for Struct {
    type Associated = u32;

    fn associated(&self, _: Self::Associated) {}
}

fn main() {
    Struct.associated(1);
    // Struct.associated("a"); // `expected u32, found reference`
}
通用关联类型是这两者的混合体。它们绑定到一个类型,恰好有1个关联生成器,而这个关联生成器反过来可以关联任意数量的类型。那么这种通用关联类型与前面示例中的通用有什么区别呢?
struct Struct;

trait GenericAssociated {
    type GenericAssociated;

    fn associated(&self, associated: Self::GenericAssociated);
}

impl<G> GenericAssociated for Struct {
    type GenericAssociated = G;

    fn associated(&self, _: Self::GenericAssociated) {}
}
2个回答

14

让我们再来看一下你上一个例子(我缩短了):

trait GenericAssociated {
    type GenericAssociated;
}

impl<G> GenericAssociated for Struct {
    type GenericAssociated = G;
}

这里并没有使用通用关联类型!你只是在impl块中拥有一个通用类型,该类型被赋值给关联类型。嗯,好的,我能看出混淆的原因所在。

你的示例会报错,错误信息为“类型参数G没有受到impl trait、self type或谓词的约束”。这种情况在实现GATs后也不会改变,因为这与GATs无关。

在你的示例中使用GATs可能会像这样:

trait Associated {
    type Associated<T>; // <-- note the `<T>`! The type itself is 
                        //     generic over another type!

    // Here we can use our GAT with different concrete types 
    fn user_choosen<X>(&self, v: X) -> Self::Associated<X>;
    fn fixed(&self, b: bool) -> Self::Associated<bool>;
}

impl Associated for Struct {
    // When assigning a type, we can use that generic parameter `T`. So in fact,
    // we are only assigning a type constructor.
    type Associated<T> = Option<T>;

    fn user_choosen<X>(&self, v: X) -> Self::Associated<X> {
        Some(x)
    }
    fn fixed(&self, b: bool) -> Self::Associated<bool> {
        Some(b)
    }
}

fn main() {
    Struct.user_choosen(1);    // results in `Option<i32>`
    Struct.user_choosen("a");  // results in `Option<&str>`
    Struct.fixed(true);        // results in `Option<bool>`
    Struct.fixed(1);           // error
}

但是要回答你的主要问题:

特质的泛型类型和关联类型之间有什么区别?

简而言之:它们允许延迟具体类型(或生命周期)的应用,从而使整个类型系统更加强大。

RFC中有许多激励性的例子,最值得注意的是流迭代器和指针族例子。让我们快速了解一下为什么无法使用特质上的通用实现流迭代器。

流迭代器的GAT版本如下:

trait Iterator {
    type Item<'a>;
    fn next(&self) -> Option<Self::Item<'_>>;
}

在当前的Rust中,我们可以将生命周期参数放在trait上而不是关联类型上:

trait Iterator<'a> {
    type Item;
    fn next(&'a self) -> Option<Self::Item>;
}

到目前为止,所有的迭代器都可以像以前一样实现这个特性。但是如果我们想要使用它呢?

fn count<I: Iterator<'???>>(it: I) -> usize {
    let mut count = 0;
    while let Some(_) = it.next() {
        count += 1;
    }
    count
}

我们应该注释哪个生命周期?除了注释'static生命周期外,我们有两种选择:
  • fn count<'a, I: Iterator<'a>>(it: I):这不起作用,因为函数的通用类型是由调用者选择的。但是it(在next调用中将变为self)存在于我们的堆栈帧中。这意味着it的生命周期对调用者来说是未知的。因此,我们会得到一个编译器错误(Playground)。这不是一个选择。
  • fn count<I: for<'a> Iterator<'a>>(it: I)(使用HRTBs):这似乎可以工作,但它有微妙的问题。现在我们要求I实现Iterator,并且适用于任何生命周期'a。这对于许多迭代器来说不是问题,但一些迭代器返回的项目不会永久存在,因此它们无法为任何生命周期实现Iterator - 只能为其项目寿命较短的生命周期实现。使用这些更高级别的特质约束通常会导致秘密的'static限制非常严格。因此,这也并不总是起作用。

正如您所看到的:我们无法正确地书写I的范围。而且,实际上,我们甚至不想在count函数的签名中提及生命周期!这是不必要的。这正是GATs允许我们做的事情(除了其他一些事情)。有了GATs,我们可以写成:

fn count<I: Iterator>(it: I) { ... }

而且它能够正常工作。因为“具体生命周期的应用”只会在我们调用 next 时发生。
如果你对更多信息感兴趣,可以查看我的博客文章“解决没有GATs的广义流迭代器问题”,其中我尝试使用特征上的通用类型来解决缺少GATs的问题。并且(剧透):它通常不起作用。

关于你提到的第二点 fn count<I: for<'a> Iterator<'a>>(it: I) — 你能否举个例子说明它如何因为过于限制(即秘密的 'static 约束)而失败? - vikram2784
@cotigao 噢,而且你可以在这个游乐场(博客文章中也有链接)中看到一个具体的例子。在最底下,我们无法将WindowMut迭代器传递给count,因为T不是“静态的”。 - Lukas Kalbertodt
谢谢您的帖子!有一个问题:引用您文章中的内容;“for<'s> T: 's which is equivalent to T:'static”⸺这是因为迭代器间接地借用了T(即通过切片本身借用具有生命周期'a的元素)吗? - vikram2784
@cotigao,我不确定我理解你的问题。for<'s> B 的意思是“对于所有可能的生命周期,约束B必须成立”。这个语句也包括“B必须在'static生命周期中成立”,对吗?因为那是“一个可能的生命周期”。而且由于'static在某种程度上“包含”所有其他生命周期,所以这两个约束是等价的。 - Lukas Kalbertodt
我尝试提供一个最小化的版本testtest2有什么区别?前者期望具体类型__只有__'static引用,而后者表示所有可能(即's)生命周期都实现了特质Trait<'s>,并相应地选择任何人?我有点困惑,因为我认为它们是一样的。 - vikram2784
显示剩余3条评论

10

有什么区别?

通用关联类型(GAT)是关联类型,其本身是泛型的。RFC从一个激励性的例子开始,重点在于:

Consider the following trait as a representative motivating example:

trait StreamingIterator {
    type Item<'a>;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

This trait is very useful - it allows for a kind of Iterator which yields values which have a lifetime tied to the lifetime of the reference passed to next. A particular obvious use case for this trait would be an iterator over a vector which yields overlapping, mutable subslices with each iteration. Using the standard Iterator interface, such an implementation would be invalid, because each slice would be required to exist for as long as the iterator, rather than for as long as the borrow initiated by next.

This trait cannot be expressed in Rust as it exists today, because it depends on a sort of higher-kinded polymorphism. This RFC would extend Rust to include that specific form of higher-kinded polymorphism, which is refered to here as associated type constructors. This feature has a number of applications, but the primary application is along the same lines as the StreamingIterator trait: defining traits which yield types which have a lifetime tied to the local borrowing of the receiver type.

请注意相关类型Item具有泛型生命周期'a。RFC中的大多数示例都使用了生命周期,但也有一个使用泛型类型的示例
trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;
    fn new<T>(value: T) -> Self::Pointer<T>;
}
请注意,相关类型Pointer具有通用类型T

您的具体示例

Generic与此通用关联类型的区别是什么?

可能没有区别,并且GAT的存在对您的情况没有帮助,因为它似乎不需要一个本身是通用的关联类型。


1
@CodeSandwich 我不确定我理解了。struct Foo 是对 struct Foo<T> 的“语法糖”吗? - Shepmaster
1
那个...不太准确。关联类型允许特质实现者选择一种类型,然后该特质就在其上操作。而泛型允许调用方选择一种类型,然后该特质在其上操作。关于谁选择以及何时选择的区别是这里的核心差异。请参见此处的问答获取更多信息。 - Zarenor
1
GAT允许特质作者要求实现者选择符合某些约束条件的泛型类型,这解决了当前系统存在的一些限制问题——答案中讨论了两个例子。 - Zarenor
但是,如果特质作者要求实现者选择不符合任何约束条件的通用类型(例如 trait Foo { type T; }),那么实现者可以自由地允许调用方选择一种类型(例如 impl<T> Foo for Bar { type T = T; }),这与常规泛型相同(例如 trait Foo<T> {})。这使得常规泛型成为GAT的边缘情况。这个推理正确吗? - CodeSandwich
2
@CodeSandwich impl<T> Foo for Bar { ... } 总是不正确的,无论你在 ... 中放入什么,与 GAT 相关或其他,都不能使其编译通过。Lukas 的答案解释了原因。在 GAT 中,impl 不是通用的,关联类型本身就是。 - trent
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接