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

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

我的理解是 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) {}
}

【问题讨论】:

    标签: generics rust generic-programming


    【解决方案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 是。
    猜你喜欢
    • 1970-01-01
    • 2020-12-02
    • 1970-01-01
    • 2015-10-24
    • 2013-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-12
    相关资源
    最近更新 更多