Appearance
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>
。 跟普通泛型参数比起来,关联类型参数必须使用名字赋值的方式。 那么,关联类型跟普通泛型参数有哪些不同点呢?我们为什么需要关联参数呢?
- 可读性可扩展性
从上面这个例子中我们可以看到,虽然我们的函数只接受一个参数 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
{
...
}
由此可见,在某些情况下,关联类型比普通泛型参数更具可读性。
- 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);
}