何时适合使用关联类型而不是泛型类型?

201
这个问题中,有人试图使用泛型类型参数,但实际上应该使用关联类型来解决问题。这引发了“为什么在这里使用关联类型更合适?”的问题,让我想知道更多信息。 引入关联类型的RFC说明:
该 RFC 通过以下方式澄清了特征匹配:
  • 将所有特征类型参数视为 输入类型,并且
  • 提供关联类型,这是 输出类型
RFC使用一个图形结构作为动机示例,在文档中也使用了相同的示例,但我承认我没有完全理解使用关联类型版本而不是类型参数化版本的好处。主要的好处是distance方法不需要关心Edge类型。这很不错,但似乎有点浅显,不能作为使用关联类型的充分理由。
我发现在实践中使用关联类型非常直观,但我在决定何时在自己的API中使用它们时会感到困难。
编写代码时,我应该在哪些情况下选择关联类型而不是通用类型参数?反之亦然?
3个回答

164
这个问题现在在《Rust编程语言(第二版)》中有所涉及。但是,我们还需要更深入地探讨一下。
让我们从一个简单的例子开始。

什么时候使用trait方法比较合适?

有多种方法可以提供延迟绑定
trait MyTrait {
    fn hello_word(&self) -> String;
}

或者:

struct MyTrait<T> {
    t: T,
    hello_world: fn(&T) -> String,
}

impl<T> MyTrait<T> {
    fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>;

    fn hello_world(&self) -> String {
        (self.hello_world)(self.t)
    }
}

忽略任何实现/性能策略,上述两个摘录都允许用户以动态方式指定hello_world的行为方式。
语义上的一个区别是,trait实现保证对于实现trait的给定类型Thello_world的行为将始终相同,而struct实现允许在每个实例上具有不同的行为。
是否使用方法取决于用例!
“关联类型”何时适用?
与上述trait方法类似,“关联类型”是一种迟绑定形式(虽然它发生在编译时),允许trait的用户指定要替换的类型。这不是唯一的方法(因此问题出现):
trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

或者:

trait MyTrait<Return> {
    fn hello_world(&Self) -> Return;
}

与上面方法的晚期绑定等效:

  • 第一个强制对于给定的Self,有单一的相关Return
  • 相反,第二个允许为Self实现多个Return

哪种形式更合适取决于是否有必要强制唯一性。例如:

  • Deref使用关联类型,因为在推断过程中如果没有唯一性,编译器会变得疯狂
  • Add使用关联类型,因为其作者认为,在给定两个参数的情况下,会有一个逻辑返回类型

正如您所看到的,虽然Deref是一个明显的用例(技术约束),但Add的情况不太明确:也许对于i32 + i32,根据上下文产生i32Complex<i32>可能是有意义的?尽管如此,作者行使了自己的判断,并决定重载加法的返回类型是不必要的。

我的个人立场是没有正确答案。尽管如此,在唯一性论点之外,我要提到相关类型使得使用trait更容易,因为它们减少了必须指定的参数数量,所以如果灵活使用常规trait参数的好处不明显,我建议从关联类型开始。

17
让我尝试简化一下:trait/struct MyTrait/MyStruct 只允许一个 impl MyTrait forimpl MyStructtrait MyTrait<Return> 是泛型的,因此允许多个 implReturn 可以是任何类型。泛型结构体也是一样的。 - Paul-Sebastian Manole
9
我发现你的回答比《Rust编程语言》中的回答更易于理解。 - drojf
1
第一个强制规定对于给定的Self,只有一个相关的Return。这在直接意义上是正确的,但当然可以通过使用通用特征进行子类化来解决此限制。也许唯一性只能是一种建议,而不能被强制执行。 - joel

59

关联类型是一种分组机制,因此应该在需要将类型分组时使用。

文档中介绍的Graph trait是一个例子。您希望Graph是通用的,但一旦您拥有特定类型的Graph,您就不想再改变NodeEdge类型。一个特定的Graph不会想要在单个实现中改变这些类型,实际上,它们需要始终保持相同。它们被分组在一起,甚至可以说是相关联的


8
我需要一些时间来理解。对我来说,这更像是同时定义几种类型:边缘和节点在图形之外没有意义。 - tafia
我还是不明白,你能详细解释一下吗? - Lance

1

关联类型可用于告诉编译器“这两个实现之间的这两种类型相同”。以下是一个双调度示例,它可以编译,并且几乎类似于标准库将迭代器与总和类型相关联的方式:

trait MySum {
    type Item;
    fn sum<I>(iter: I)
    where
        I: MyIter<Item = Self::Item>;
}

trait MyIter {
    type Item;
    fn next(&self) {}
    fn sum<S>(self)
    where
        S: MySum<Item = Self::Item>;
}

struct MyU32;

impl MySum for MyU32 {
    type Item = MyU32;

    fn sum<I>(iter: I)
    where
        I: MyIter<Item = Self::Item>,
    {
        iter.next()
    }
}

struct MyVec;

impl MyIter for MyVec {
    type Item = MyU32;
    fn sum<S>(self)
    where
        S: MySum<Item = Self::Item>,
    {
        S::sum::<Self>(self)
    }
}

fn main() {}


此外,https://blog.thomasheartman.com/posts/on-generics-and-associated-types 也提供了一些有关此问题的好信息:
简而言之,当您希望类型 A 能够为不同的类型参数实现特征(例如 From 特征)时,请使用泛型。
如果一个类型只需要实现一次特征,则使用关联类型,例如 Iterator 和 Deref。请注意保留 HTML 标记。

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