什么时候适合使用关联类型和泛型类型?

这个问题中,出现了一个问题,可以通过将使用泛型类型参数的尝试更改为关联类型来解决这个问题。这引发了一个问题: “为什么关联类型在这里更合适?”这让我想知道更多。

引入关联类型的 RFC表示:

这个 RFC 通过以下方式澄清了特征匹配:

  • 将所有特征类型参数视为 输入类型,以及
  • 提供关联的类型,即 输出类型

RFC 使用图形结构作为一个激励人心的例子,文件中也使用了这个例子,但是我承认没有充分认识到关联类型版本相对于类型参数化版本的好处。主要的是,distance方法不需要关心 Edge类型。这很好,但是对于拥有关联类型的理由似乎有点肤浅。

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

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

22596 次浏览

关联类型是 分组机制分组机制,因此应该在对类型进行分组时使用它们。

文档中介绍的 Graph trait 就是这样一个例子。你希望 Graph是通用的,但是一旦你有了一个特定的 Graph类型,你就不希望 Node或者 Edge类型变化了。特定的 Graph不希望在单个实现中改变这些类型,事实上,希望它们总是相同的。它们被组合在一起,或者可以说是 相关的

这现在在 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
  • 相反,第二种方法允许对多个 Return实现 SelfMyTrait

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

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

正如您所看到的,虽然 Deref是一个明显的用例(技术限制) ,但是 Add的情况不那么明确: 也许 i32 + i32根据上下文产生 i32Complex<i32>是有意义的?尽管如此,提交人行使了他们的判决,认为没有必要为增加内容而超载返回类型。

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

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

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以便能够为不同的类型参数实现一个 trait 的任意次数时,使用泛型,例如 From trait 的情况。

如果类型只实现一次 trait 有意义,则使用关联类型,例如使用 Iterator 和 Deref。