trait 的泛型类型和泛型关联类型之间有啥区别?

Posted

技术标签:

【中文标题】trait 的泛型类型和泛型关联类型之间有啥区别?【英文标题】:What's the difference between a trait's generic type and a generic associated type?trait 的泛型类型和泛型关联类型之间有什么区别? 【发布时间】:2019-07-14 11:02:50 【问题描述】:

这个问题是在 Rust 中可用的泛型关联类型之前提出的,尽管它们是 proposed 和 developed。

我的理解是 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");

关联类型只绑定一种类型:

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 关联的生成器,而生成器又可以关联任意数量的类型。那么上例中的Generic和这个泛型关联类型有什么区别呢?

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) 

【问题讨论】:

【参考方案1】:

让我们再看看你的最后一个例子(被我缩短了):

trait GenericAssociated 
    type GenericAssociated;


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

具有通用关联类型!您只是在分配给关联类型的 impl 块上有一个泛型类型。嗯,好的,我知道混乱来自哪里了。

您的示例错误“类型参数 G 不受 impl trait、self 类型或谓词的约束”。这在实施 GAT 时不会改变,因为这与 GAT 无关。

在您的示例中使用 GAT 可能如下所示:

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


但要回答你的主要问题:

特征的泛型类型和泛型关联类型有什么区别?

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

the RFC 中有很多激励性的示例,最值得注意的是流式迭代器和指针族示例。让我们快速了解为什么不能在 trait 上使用泛型实现流式迭代器。

流式迭代器的 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&lt;'a, I: Iterator&lt;'a&gt;&gt;(it: I):这不起作用,因为调用者选择了函数的泛型类型。但是it(在next 调用中将变为self)存在于我们的 堆栈框架中。这意味着调用者不知道it 的生命周期。因此我们得到了一个编译器(Playground)。这不是一个选项。 fn count&lt;I: for&lt;'a&gt; Iterator&lt;'a&gt;&gt;(it: I)(使用 HRTB):这似乎可行,但存在一些微妙的问题。现在我们需要I 来实现Iteratorany 生命周期'a。这对许多迭代器来说不是问题,但一些迭代器返回的项目不会永远存在,因此它们无法在 any 生命周期内实现 Iterator —— 只是生命周期比它们的项目短。使用这些排名较高的特征边界通常会导致非常严格的秘密 'static 边界。所以这也并不总是有效。

如您所见:我们无法正确记下I 的界限。实际上,我们甚至不想在 count 函数的签名中提及生命周期!这不应该是必要的。这正是 GAT 允许我们做的事情(除其他外)。使用 GAT,我们可以编写:

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

它会起作用的。因为“具体生命周期的应用”只有在我们调用next时才会发生。

如果您对更多信息感兴趣,可以查看my blog post “ Solving the Generalized Streaming Iterator Problem without GATs”,我尝试在特征上使用泛型类型来解决 GAT 不足的问题。而且(剧透):它通常不起作用。

【讨论】:

关于你提到的第二点fn count&lt;I: for&lt;'a&gt; Iterator&lt;'a&gt;&gt;(it: I)——你能否提供一个例子说明它会因为限制而失败(即秘密'static bound)? @cotigao 我更详细地描述了限制in this blog post(链接有一个指向正确部分的锚)。简而言之:如果你想让你的项目引用一个具有迭代器生命周期的泛型(即impl&lt;'s, T&gt; Iterator&lt;'s&gt; for ... type Item = &amp;'s T; ),你必须绑定T: 's。否则 Rust 不会让你编写那个引用类型。边界for&lt;'a&gt; I: Iterator&lt;'a&gt; 加上T: 's 导致T: 'static。我希望这会有所帮助! @cotigao 哦,你可以看到一个具体的例子in this playground(也在博客文章中链接)。在最底部,我们不能将 WindowMut 迭代器传递给 count,因为 T 不是 'static 感谢您的帖子!一个问题:引用你的文章; " for&lt;'s&gt; T: 's 相当于 T:'static " ⸺ 是否相等,因为迭代器间接借用了T(即通过本身借用生命周期为'a 的元素的切片)? @cotigao 我不确定我是否理解你的问题。 for&lt;'s&gt; B 的意思是“对于所有可能的生命周期,绑定 B 必须保持”。该语句还包括“B必须为'static保留”,对吗?因为那是“可能的一生”。由于'static 在某种意义上“包括”了所有其他生命周期,因此这两个边界是等价的。【参考方案2】:

有什么区别?

通用关联类型 (GAT) 是关联类型,它们本身就是通用。 RFC 以motivating example 开头,重点是我的:

将以下特质视为具有代表性的激励示例:

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

这个特性非常有用 - 它允许一种迭代器 产生生命周期与生命周期相关的值 引用传递给next。一个特别明显的用例 特征将是对产生重叠的向量的迭代器, 每次迭代的可变子切片。使用标准Iterator 接口,这样的实现将是无效的,因为每个 只要迭代器存在,切片就必须存在,而不是 比next发起的借用时间长。

这个特性不能在今天的 Rust 中表达,因为 它依赖于一种更高种类的多态性。该 RFC 将 扩展 Rust 以包含更高种类的特定形式 多态性,这里称为关联类型 构造函数。此功能有许多应用程序,但 主要应用程序与 StreamingIterator trait:定义产生类型的特征 与接收器类型的本地借用相关的生命周期。

请注意关联类型 Item 如何具有通用生命周期 'a。 RFC 中的大多数示例都使用生命周期,但也有 an example using a generic type:

trait PointerFamily 
    type Pointer<T>: Deref<Target = T>;
    fn new<T>(value: T) -> Self::Pointer<T>;

注意关联类型 Pointer 如何具有泛型类型 T

你的具体例子

上例中的Generic 和这个泛型关联类型有什么区别

可能没有,GAT 的存在对您的情况没有帮助,这似乎不需要关联类型本身就是通用的。

【讨论】:

这使得“常规”泛型只是泛型关联类型的语法糖,不是吗?我有点惊讶 Rust 试图引入第二种方式来表达同样的事情,通常尽可能避免。 @CodeSandwich 我不确定我是否遵循。 struct Foo“语法糖”是否超过struct Foo&lt;T&gt; 那是……不太准确。关联类型允许 trait 实现者 选择一个类型,然后 trait 对其进行操作。泛型类型允许 被调用者 选择一个类型,该特征对其进行操作。关于谁选择以及何时选择的区别是这里差异的核心。更多信息请参阅问答here GAT 允许 trait author 要求 implementors 选择满足某些约束的 generic 类型 - 这解决了一个问题作为当前系统的限制而出现的问题 - 答案中讨论了两个示例。 @CodeSandwich impl&lt;T&gt; Foo for Bar ... 总是不正确的,并且没有任何东西,无论是与 GAT 相关还是其他,您都可以放入 ... 以使其编译。卢卡斯的回答解释了原因。在 GAT 中,implnot 泛型的,关联类型 itself 是。

以上是关于trait 的泛型类型和泛型关联类型之间有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

Java里的泛型加通配符的用法

C#中的泛型和泛型集合

如何关联高阶函数之间定义的泛型类型?

Java里的泛型加通配符的用法

java里的泛型和通配符

泛型类的基本使用