为什么要有 Associated Type?

背景

什么是 Associated Type?

Associated Type 是泛型的一个子概念。在 Rust 里 Associated Type 和 Trait 绑定在一起,指定输出类型 (output type)。

Rust Book 里的一个例子是:


#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
}

这样写,Counter里的Self::Item就指代的是u32

而更详细的介绍请看Rust Book 对应章节

答案

短答案是:

  • 带有 Associated Type 的 Trait 只能被一个类型impl 一次,所以可以避免一个类型有多个impl
  • Associated Type 可以当做 Output Type
  • Associated Type 带来工程上的便利

如果短答案总觉得是隔靴搔痒,那么我们需要问以下子问题:

  1. 既然 Associated Type 是泛型的子概念,那么 Associated Type 和 Rust 泛型有什么不同?
  2. 什么是 Input Type 和 Output Type?
  3. 为什么带有 Associated Type 的 Trait 只能被一个类型impl 一次?
  4. 普通泛型可以代替 Associated Type 吗?如果可以,那为什么还要 Associated Type?

对于第一个问题,它们的不同主要有两点:

  • 普通泛型可以用于 Trait, Struct和函数,但是 Associated Type 只能与 Trait 绑定。

  • 带有 Associated Type 的 Trait 只能被一个类型impl 一次,但是带有普通泛型的 Trait 可以有多个impl,例如

    
    #![allow(unused)]
    fn main() {
    // 普通泛型 + Trait
    pub trait GiveMeSomething<T: Clone> {
        fn get_something(&self) -> T;
    }
    
    // Associated Type + Trait
    pub trait GiveMeData {
        type Data;
        fn get_some_data(&self) -> Self::Data;
    }
    
    // 普通泛型 + Struct
    pub struct Something<T: Clone> {
        data: T,
    }
    
    // 一个 struct 可以有多个 GiveMeSomething<T> 的 impl
    impl<T: Clone> GiveMeSomething<u8> for Something<T> {
        fn get_something(&self) -> u8 {
            1
        }
    }
    
    impl<T: Clone> GiveMeSomething<i32> for Something<T> {
        fn get_something(&self) -> i32 {
            -1
        }
    }
    
    
    impl<T: Clone> Something<T> {
        // 普通泛型 + 函数
        pub fn get_data(&self) -> T {
            self.data.clone()
        }
    }
    
    // 一个 struct 只能有一个 GiveMeData 的 impl
    impl GiveMeData for Something<u8> {
        type Data = u8;
    
        fn get_some_data(&self) -> Self::Data {
            self.data
        }
    }
    }
    

如果我们把泛型的类型(也就是<T>中的T)也作为输入的话,那么我们可以把get_data()的输入看成是(T, &self),把它的输出看成是(T, T),第一个T是类型,第二个T是指属于这个类型的值。

假如说我们有一个Something<u8>的示例,它存着data = 1,那么它的get_data()的输入就是(u8, &self),输出就是(u8, 1)

那么,很简单的,输入的类型就是 Input Type,输出的类型就是 Output Type,也就是,出现在参数的类型是 Input Type,出现在返回值的类型是 Output Type。

至于第三个问题,为什么带有 Associated Type 的 Trait 只能被一个类型impl 一次,我们需要一些实际的例子。典型的例子就是上面的pub trait Iterator。对于Iterator,很自然的,我们想要遍历某个实例的所有的值,那么这些值只有一个类型,所以对于一个类型来说,这个 Trait 只能被impl一次。

第四个问题和第三个问题紧密相关。实际上,如果我们能人为地保证带有普通泛型的 Trait 只被一个类型 impl 一次,那么我们完全可以用普通泛型代替 Associated Type。那么为什么我们还要发明一个 Associated Type 呢?

第一个理由是最简单的,你不能人为地保证带有普通泛型的 Trait 只被一个类型 impl 一次。万一一个实习生不懂,为了方便就随手加多了一个impl呢?假如说这个乌龙发生在Iterator这个 Trait 上面,而恰好你也在用着它(例如for item in my_custom_list {}),那么rustc会抱怨不知道用哪个impl,然后一个编译错误就产生了。如果你想rustc帮你检查并保证这个带有普通泛型的 Trait 只被一个类型 impl 一次,那么这就实际上变成了 Associated Type 了。

更多的理由我们需要举个例子。Rust RFC Book 里的例子已经非常好了,所以我在这里简要翻译一下,并且补充一些内容。

假设我们需要建图,并且表达成一个 Trait Graph。如果仅仅使用普通泛型,我们可以写成


#![allow(unused)]
fn main() {
// N 和 E 是节点和边的类型
trait Graph<N, E> {
    fn has_edge(&self, &N, &N) -> bool;
    ...
}
}

但是如果这样写的话,一个类型可以有多个Graphimpl,并且每个implNE 都不一样。这就有点诡异了,因为对于一个实际的图,它的节点和边的类型是唯一确定的。并且很自然地,NE这两个类型就应该和Graph绑定在一起,有个从属的关系。

而且,假如说我们要写个函数,计算两节点间的距离,使用普通泛型,我们要这样写


#![allow(unused)]
fn main() {
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> uint { ... }
}

前面泛型的部分就会有一长串,就是<N, E, G<N, E>>,如果一个 Trait 有好几个泛型,那么情况就更糟了,可能<>里的内容就要写一行。

而如果我们用 Associated Type,我们可以这样写


#![allow(unused)]
fn main() {
trait Graph {
    type N;
    type E;
    fn has_edge(&self, &N, &N) -> bool;
}
}

然后计算距离的函数就可以写成


#![allow(unused)]
fn main() {
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> uint { ... }
}

这样代码就变得更加简洁了。

另外,对比两个distance函数,第一个函数根本就没用到类型E,但是因为语法要求必须要写上,而第二个用G::N就行,可以不用写用不上的G::E

除了少写用不上的代码这个好处之外,我们写代码的时候还有更多灵活性和可扩展性。假如说我们要拓展我们的Graph,变成


#![allow(unused)]
fn main() {
trait Graph{
    type N;
    type E;
    type A;
    type B;
    type C;
    fn has_edge(&self, &N, &N) -> bool;
}
}

如果使用普通泛型的话,第一个distance函数就不可避免地要加上A, B, C这些泛型标识,否则编译就会出问题,但是使用 Associated Type 的话,第二个distance函数根本就不用动。

综上,Rust 的 Associated Type 不是普通泛型的语法糖,而是经过深思熟虑的、能够解决实际问题并且优化代码的实现。

相关链接及参考链接

  • Associated Type 的 Rust RFC,里面有更加详细的解释,并且还列举了Associated Type 另外两个优点(虽然对应用开发者影响不大)
  • Rust Forum 里的相关讨论帖子
  • Rust Book 里的相关章节