Skip to content

21.5 关联类型

trait 中不仅可以包含方法(包括静态方法)、常量,还可以包含“类型”。最常见的莫过于关联类型:

rust
trait MyTrait {
    // 关联类型
    type MyType;

    fn my_fn(&self, arg: Self::MyType) -> Self::MyType;
}

关联类型(Associated Types)是指在 trait 中定义一个或多个类型,这些类型可以在 trait 的方法中使用,并将具体的类型实现留给实现该 trait 的类型。 我们可以将关联类型看作是 trait 中的占位符,用于表示某些方法的输入或输出类型,这样就可以在实现 trait 的时候具体确定这些类型。 在 trait 方法中使用时,关联类型可以通过 Self::MyType 的形式来引用。

只有指定了所有的泛型参数和关联类型,这个 trait 才能真正地具体化。 示例如下(在泛型函数中,使用 Iterator 泛型作为泛型约束):

rust
use std::iter::Iterator;
use std::fmt::Debug;

fn use_iter<ITEM, ITER>(mut iter: ITER)
    where ITER: Iterator<Item=ITEM>,
            ITEM: Debug
{
    while let Some(i) = iter.next() {
        println!("{:?}", i);
    }
}

fn main() {
    let v: Vec<i32> = vec![1,2,3,4,5];
    use_iter(v.iter());
}

可以看到,我们希望参数是一个泛型迭代器,可以在约束条件中写 Iterator<Item=ITEM>跟普通泛型参数比起来,关联类型参数必须使用名字赋值的方式。 那么,关联类型跟普通泛型参数有哪些不同点呢?我们为什么需要关联参数呢?

  1. 可读性可扩展性

从上面这个例子中我们可以看到,虽然我们的函数只接受一个参数 iter,但是它却需要两个泛型参数:一个用于表示迭代器本身的类型,一个用于表示迭代器中包含的元素类型。这是相对冗余的写法。 实际上,在有关联类型的情况下,我们可以将上面的代码简化,示例如下:

rust
use std::iter::Iterator;
use std::fmt::Debug;

fn use_iter<ITER>(mut iter: ITER)
    where ITER: Iterator,
            ITER::Item: Debug
{
    while let Some(i) = iter.next() {
        println!("{:?}", i);
    }
}

fn main() {
    let v: Vec<i32> = vec![1,2,3,4,5];
    use_iter(v.iter());
}

这个版本的写法相对于上一个版本来说,泛型参数明显简化了,我们只需要一个泛型参数即可。在泛型约束条件中,可以写上 ITER 符合 Iterator 约束。此时,我们就已经知道 ITER 存在一个关联类型 Item,可以针对这个 ITER::Item 再加一个约束即可。如果我们的 Iterator 中的 Item 类型不是关联类型,而是普通泛型参数,就没办法进行这样的简化了。

我们再看另一个例子。假如我们想设计一个泛型的“图”类型,它包含“顶点”和“边”两个泛型参数,如果我们把它们作为普通的泛型参数设计,那么看起来就是:


rust
trait Graph<N, E> {
    fn has_edge(&self, &N, &N) -> bool;
    ...
}

现在如果有一个泛型函数,要计算一个图中两个顶点的距离,它的签名会是:


rust
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> uint {
    ...
}

我们可以看到,泛型参数比较多,也比较麻烦。对于指定的 Graph 类型,它的顶点和边的类型应该是固定的。 在函数签名中再写一遍其实没什么道理。如果我们把普通的泛型参数改为“关联类型”设计,那么数据结构就成了:


rust
trait Graph {
    type N;
    type E;
    fn has_edge(&self, &N, &N) -> bool;
    ...
}

对应的,计算距离的函数签名可以简化成:


rust
fn distance<G>(graph: &G, start: &G::N, end: &G::N) -> uint
    where G: Graph
{
    ...
}

由此可见,在某些情况下,关联类型比普通泛型参数更具可读性。

  1. trait 的 impl 匹配规则

泛型的类型参数,既可以写在尖括号里面的参数列表中,也可以写在 trait 内部的关联类型中。这两种写法有什么区别呢?我们用一个示例来演示一下。

假如我们要设计一个 trait,名字叫作 MyTrait,用于类型转换。那么,我们就有两种选择。一种是使用泛型类型参数:

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

另一种是使用关联类型:

rust
trait ConvertTo {
    type DEST = f32;
    fn convert(&self) -> Self::DEST;
}

假如我们想继续增加一种从 i32 类型到 f64 类型的转换,使用泛型参数来实现的话:

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

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

fn main() {
    let x: i32 = 42;
    let y: f64 = x.convert();
    println!("{} converted to {}", x, y);
}

如果用关联类型来实现的话:

rust
trait ConvertTo {
    type DEST;
    fn convert(&self) -> Self::DEST;
}

impl ConvertTo for i32 {
    type DEST = f64;
    fn convert(&self) -> Self::DEST { *self as Self::DEST }
}

fn main() {
    let x: i32 = 42;
    let y: f64 = x.convert();
    println!("{} converted to {}", x, y);
}

Released under the MIT License