Rust中的泛型关联类型

定义

我们先来统一语义,什么叫泛型 ,而什么又叫 泛型关联类型

泛型

1
2
3
struct Cup<T> {
t: T
}

这个例子描述了可以盛液体的杯子。我们可以用他来盛水,盛牛奶

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let c = Cup { t: COFFEE };
let c = Cup { t: MILK };
}

struct MILK;

struct COFFEE;

struct Cup<T> {
t: T,
}

我们在实例化时必须指定T的类型。泛型可以添加约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// let c = Cup { t: COFFEE };
let c = Cup { t: MILK };
}

struct MILK;

impl White for MILK{}
struct COFFEE;

struct Cup<T:White> {
t: T,
}

trait White {
}

我们在创建Cup时也必须明确一个满足White的编译时能明确的类型,而不能是一个满足White的编译不知道的类型。如果有类似编译器不明确的类型应该使用trait object。

泛型能够令你的数据结构支持多种类型,比如我们在设计一个数据容器,比如说List时,并不清楚这个数据结构使用者会将什么类型塞进来,所以这里可以使用泛型当做占位符。对于Rust会要求我们的类型需要满足编译时大小可知,所以这个占位符必须在编译时,实例化时能够确定。

泛型不止可以指定占位类型,还可以支持生命周期,生命周期的细节我们后续再聊。那么聊了这么多泛型,为什么我们需要generic-associated-type (泛型关联类型 GAT)。先来看一个经典的例子,假设我们要仅用泛型实现一个迭代器:

1
2
3
trait MyIterator {

}

泛型关联类型

关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。

1
2
3
4
5
6
7
8
9
10
11
12
pub trait Watch {
type Item;
fn inner(&self) -> Option<Self::Item>;
}

impl Watch for A {
type Item = i32;
fn inner(&self) -> Option<Self::Item> {
Some(self.data)
}
}

而同样基于泛型实现的Watch

1
2
3
4
5
6
7
8
9
trait Watch<T> {
fn inner(&self) -> Option<T>;
}

impl Watch<i32> for A {
fn inner(&self) -> Option<i32> {
Some(self.data)
}
}

使用泛型关联类型有几个好处。首先,在trait定义的过程中,可以当做类型别名来用,尤其是你的泛型类型是很多trait的约束时,直接使用关联类型来占位会非常直观简洁。整体提高代码可读性。

1
2
3
4
pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable {
type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash;
fn is_null(&self) -> bool;
}

其次就是某些特殊的数据结构需要我们将结构体本身的生命周期和关联类型的生命周期分开。

GAT让我们在impl trait的时候就必须指定关联类型,比起泛型到最后实例化在指定类型提前了一些。
对泛型来说:

  1. 定义trait时,定义占位符X
  2. impl trait时,可以指定X,可以不指定X
  3. 实例化impl了trait的东西时,如果第2步没指定的,此时必须要指定X了。

对于关联类型来说:

  1. 定义trait时,定义占位符X
  2. impl trait时就必须要指定X
  3. 实例化impl了trait的东西时,不必指定X。

从第二步的语义上讲也符合GAT的定义,这里确确实实时将类型和trait绑在一起,trait实现则GAT确定。

何时使用GAT?

我们已经看到,泛型和关联类型在很多使用场合是重叠的,但是选择使用泛型还是关联类型是有原因的。
https://github.com/rust-lang/rfcs/blob/master/text/0195-associated-items.md

This RFC clarifies trait matching by:

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

这里rfc设计为类型参数即泛型为入参,而关联类型为出参。再有就是前面提到的,如果你能保证在impl trait时 用户就能唯一确认其类型,那么就可以使用GAT。