Skip to content

21.6 何时使用关联类型

从前文中大家可以看到,虽然关联类型也是类型参数的一种,但它与泛型类型参数列表是不同的。我们可以把这两种泛型类型参数分为两个类别:

  • 输入类型参数
  • 输出类型参数

在尖括号中存在的泛型参数,是输入类型参数;在 trait 内部存在的关联类型,是输出类型参数。输入类型参数是用于决定匹配哪个 impl 版本的参数;输出类型参数则是可以由输入类型参数和 Self 类型决定的类型参数。

继续以上面的例子为例,用泛型参数实现的版本如下:


rust
trait ConvertTo<T> {
    fn convert(&self) -> T;
}

impl ConvertTo<f32> for i32 {
    fn convert(&self) -> f32 { *self as f32 }
}

impl ConvertTo<f64> for i32 {
    fn convert(&self) -> f64 { *self as f64 }
}

fn main() {
    let i = 1_i32;
    let f = i.convert();
    println!("{:?}", f);
}

编译的时候,编译器会报错:


error: unable to infer enough type information about `_`; type annotations or generic parameter binding required

因为编译器不知道选择使用哪种convert方法,需要我们为它指定一个类型参数,比如:

rust
let f : f32 = i.convert();
// 或者
let f = ConvertTo::<f32>::convert(&i);

这很像 C++/Java 等语言中存在的“函数重载”规则。我们可以用不同的参数类型实现重载,但是不能用不同的返回类型来做重载,因为编译器是根据参数类型来判断调用哪个版本的重载函数的,而不是依靠返回值的类型。

在标准库中,何时使用泛型参数列表、何时使用关联类型,实际上有非常好的示范。

以标准库中的 AsRef 为例。我们希望 String 类型能实现这个 trait,而且既能实现String::as_ref::<str>()也能实现String::as_ref::<[u8]>()。因此 AsRef 必须有一个类型参数,而不是关联类型。这样impl AsRef<str>for Stringimpl AsRef<[u8]> for String才能同时存在,互不冲突。如果我们把目标类型设计为关联类型,那么针对任何一个类型,最多只能 impl 一次,这就失去 AsRef 的意义了。

我们再看标准库中的 Deref trait。我们希望一个类型实现 Deref 的时候,最多只能 impl 一次,解引用的目标类型是唯一固定的,不要让用户在调用obj.deref()方法的时候指定返回类型。因此 Deref 的目标类型应该设计为“关联类型”。否则,我们可以为一个类型实现多次 Deref,比如impl Deref<str> for Stringimpl Deref<char> for String,那么针对 String 类型做解引用操作,在不同场景下可以有不同的结果,这显然不是我们希望看到的。解引用的目标类型应该由 Self 类型唯一确定,不应该在被调用的时候被其他类型干扰。这种时候应使用关联类型,而不是类型参数。关联类型是在 impl 阶段确定下来的,而不是在函数调用阶段。这样才是最符合我们需求的写法。

还有一些情况下,我们既需要类型参数,也需要关联类型。比如标准库中的各种运算符相关的 trait。以加法运算符为例,它对应的 trait 为std::ops::Add,定义为:

rust
trait Add<RHS=Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}

在这个 trait 中,“加数”类型为 Self,“被加数”类型被设计为类型参数 RHS,它有默认值为 Self,求和计算结果的类型被设计为关联类型 Output。 用前面所讲解的思路来分析可以发现,这样的设计是最合理的方式。“被加数”类型在泛型参数列表中,因此我们可以为不同的类型实现 Add 加法操作,类型 A 可以与类型 B 相加,也可以与类型 C 相加。 而计算结果的类型不能是泛型参数,因为它是被 Self 和 RHS 所唯一固定的。它需要在 impl 阶段就确定下来,而不是等到函数调用阶段由用户指定,是典型的“输出类型参数”。

  1. 实现不同

如果 trait 中包含泛型参数,那么可以对同一个目标类型多次 impl 此 trait,每次提供不同的泛型参数。 关联类型方式只允许对目标类型实现一次。

如果 trait 中包含泛型参数,那么在具体方法调用的时候,必须加以类型标注以明确使用的是哪一个具体的实现。 而关联类型方式具体调用时不需要标注类型(因为不存在模棱两可的情况)。

  1. 适用场景

如果针对特定类型的 trait 有多个实现(例如 From),则使用泛型;否则使用关联类型(例如 IteratorDeref)。

例如在定义通用容器时,可以使用关联类型来表示容器内部的元素类型。 这样可以增强代码的可读性,因为它可以将容器内部的类型移动到 trait 中作为输出类型。

关联类型有许多优点。首先,它可以增强代码的可读性,因为它可以将容器内部的类型移动到 trait 中作为输出类型。 其次,它可以减少代码的冗余,因为它允许我们在定义通用 trait 时省略一些不必要的泛型参数。 此外,它还可以提高代码的灵活性,因为它允许我们在实现 trait 时指定关联类型的具体类型。

总体来说,关联类型更多地用于 trait 的定义中,目的是为了提供更灵活、更清晰的抽象机制。 泛型则可以用于更加通用的场景,例如函数、结构体等,帮助我们实现更加灵活、可复用的代码。

Released under the MIT License