什么时候使用关联类型与泛型类型比较合适?

When is it appropriate to use an associated type versus a generic type?

中出现了一个问题,可以通过将使用泛型类型参数的尝试更改为关联类型来解决。这引发了问题 "Why is an associated type more appropriate here?",这让我想了解更多。

RFC that introduced associated types 说:

This RFC clarifies trait matching by:

  • Treating all trait type parameters as input types, and
  • Providing associated types, which are output types.

RFC 使用图形结构作为激励示例,the documentation 中也使用了它,但我承认我没有完全理解关联类型版本相对于类型参数化版本的好处版本。最主要的是 distance 方法不需要关心 Edge 类型。这很好,但似乎根本没有关联类型的原因。

我发现关联类型在实践中使用起来非常直观,但我发现自己在决定何时何地在自己的应用程序中使用它们时遇到困难 API。

编写代码时,什么时候应该选择关联类型而不是泛型类型参数,什么时候应该相反?

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

文档中介绍的 Graph 特性就是一个例子。您希望 Graph 是通用的,但是一旦您有了特定类型的 Graph,您就不希望 NodeEdge 类型再变化。特定的 Graph 不希望在单个实现中改变这些类型,事实上,希望它们始终相同。它们被组合在一起,或者甚至可以说 associated.

现在 the second edition of The Rust Programming Language 中提到了这一点。但是,让我们深入探讨一下。

让我们从一个更简单的例子开始。

When is it appropriate to use a trait method?

有多种方式提供后期绑定:

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

忽略任何 implementation/performance 策略,上面的两个摘录都允许用户以动态方式指定 hello_world 应该如何表现。

一个区别(语义上)是 trait 实现保证对于给定类型 T 实现 traithello_world 将始终具有相同的行为而 struct 实现允许在每个实例的基础上有不同的行为。

使用方法是否合适取决于用例!

When is it appropriate to use an associated type?

与上面的 trait 方法类似,关联类型是后期绑定的一种形式(尽管它发生在编译时),允许 trait 的用户为给定实例指定类型来替代。这不是唯一的方法(因此是问题):

trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

或:

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

相当于上面方法的后期绑定:

  • 第一个强制执行对于给定的 Self 有一个 Return 关联
  • 第二个,相反,允许实现 MyTrait for Self for multiple Return

哪种形式更合适取决于强制单一性是否有意义。例如:

  • Deref 使用关联类型,因为如果没有唯一性,编译器会在推理过程中发疯
  • Add 使用关联类型,因为它的作者认为给定两个参数会有一个合乎逻辑的 return 类型

如您所见,虽然 Deref 是一个明显的用例(技术限制),但 Add 的情况不太明确:也许 i32 + i32 有意义根据上下文产生 i32Complex<i32>?尽管如此,作者进行了判断并决定重载 return 类型以进行添加是不必要的。

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

关联类型可以用来告诉编译器“这两种实现之间的这两种类型是相同的”。这是一个编译的双重分派示例,几乎类似于标准库将迭代器与求和类型相关联的方式:

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 也有一些很好的信息:

In short, use generics when you want to type A to be able to implement a trait any number of times for different type parameters, such as in the case of the From trait.

Use associated types if it makes sense for a type to only implement the trait once, such as with Iterator and Deref.