Skip to content

高级trait

在 trait 的定义中使用关联类型指定占位类型

关联类型(associated type)是 trait 中的类型占位符,它可以被用于 trait 的方法签名中。
trait 的实现者需要根据特定的场景来为关联类型指定具体的类型。
通过这一技术,我们可以定义出包含某些类型的 trait,而无须在实现前确定它们的具体类型是什么。

关联类型在诸多高级特性中更为常用。

标准库中的Iterator就是一个带有关联类型的 trait 示例,它拥有一个名为Item的关联类型,并使用该类型来替代迭代中出现的值类型。

Iterator trait的定义如下所示:

1
2
3
4
5
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

这里的类型Item是一个占位类型,而 next 方法的定义则表明它会返回类型为Option<Self::Item>的值。
Iterator trait的实现者需要为Item指定具体的类型,并在实现的next方法中返回一个包含类型值的Option

关联类型看起来与泛型的概念有点类似,后者允许我们在不指定具体类型的前提下定义函数。
那么我们为什么需要使用关联类型呢?

让我们通过一个例子来观察它们两者之间的区别

1
2
3
4
5
6
impl Iterator for Counter {
    type Item = u32;

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

这里的语法似乎和泛型语法差不多,那么我们为什么不直接使用泛型来定义Iterator trait呢?如下所示:

1
2
3
pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

其中的区别在于,如果我们使用了示例中的泛型版本,那么就需要在每次实现该 trait 的过程中标注类型;
因为我们既可以实现Iterator<String> for Counter,也可以实现其他任意的迭代类型,从而使得Counter可以拥有多个不同版本的Iterator实现。
换句话说,当 trait 拥有泛型参数时,我们可以为一个类型同时多次实现 trait,并在每次实现中改变具体的泛型参数。
那么当我们在Counter上使用next方法时,也必须提供类型标注来指明想要使用的Iterator实现。

借助关联类型,我们不需要在使用该 trait 的方法时标注类型,因为我们不能为单个类型多次实现这样的 trait。
由于我们只能实现一次impl Iterator for Counter,所以Counter就只能拥有一个特定的Item类型。
我们不需要在每次调用Counternext方法时来显式地声明这是一个u32类型的迭代器。

默认泛型参数和运算符重载

我们可以在使用泛型参数时为泛型指定一个默认的具体类型。
当使用默认类型就能工作时,该 trait 的实现者可以不用再指定另外的具体类型。
你可以在定义泛型时通过语法<PlaceholderType=Concreate Type>类为泛型指定默认类型。

这个技术常常被应用在运算符重载中。
运算符重载使我们可以在某些特定的情形下自定义运算符(比如 +)的具体行为。

虽然 Rust 不允许你创建自己的运算符及重载任意的运算符,但你可以实现std::ops中列出的那些 trait 来重载一部分相应的运算符。
例如,在下例中,我们为Point结构体实现的Add trait重载了 + 运算符,它允许代码对两个Point实例执行加法操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 });
}

add方法将两个Point实例的 x 与 y 分别相加来创建出一个新的 Point。
Add trait拥有一个名为Output的关联类型,它被用来确定add方法的返回类型。

这里的Add trait使用了默认泛型参数,它的定义如下所示:

1
2
3
4
5
trait Add<RHS=Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

你应该对这段代码中的大部分语法都较为熟悉,它定义的 trait 中带有一个方法和一个关联类型。
那段新的语法RHS=Self就是所谓的默认参数类型。
泛型参数 RHS(也就是 "right-handle side"的缩写)定义了 add 方法中 rhs 参数的类型。
假如我们在实现 Add trait 的过程中没有为 RHS 指定一个具体的类型,那么 RHS 的类型就会默认为 Self,也就是我们正在为其实现 Add trait 的那个类型。

因为我们希望将两个 Point 实例相加,所以代码在为 Point 实现 Add 时使用了默认的 RHS。
现在让我们来看另外一个例子,这个新的例子会在实现 Add trait 时自定义 RHS 的类型而不使用其默认类型。

这里有两个以不同单位存放值的结构体:MillimetersMeters
我们希望可以将毫米表示的值与米表示的值相加,并在 Add 的实现中添加正确的转换计算。
我们可以为 Millimeters 实现 Add,并将 Meters 作为 RHS,如示例所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::ops::Add;

#[derive(Debug)]
struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + other.0 * 1000)
    }
}

fn main() {
    let a = Millimeters(10);
    let b = Meters(1);
    println!("{:?}", a + b);
}
// Millimeters(1010)

为了将 Millimeters 和 Meters 的值加起来,我们指定impl Add<Meters>来设置 RHS 类型参数的值,而没有使用默认的 Self。

默认类型参数主要被用于以下两种场景:

  • 扩展一个类型而不破坏现有代码
  • 允许在大部分用户都不需要特定场合进行自定义

标准库中的 Add trait 就是第二种场景的例子:通常你只需要将两个同样类型的值相加,但 Add trait 也同时提供了自定义额外行为的能力。
在 Add trait 的定义中使用默认类型参数意味着,在大多数情况下你都不需要指定额外的参数。
换句话说,就是可以避免一小部分重复的代码模块,从而可以更加轻松地使用 trait。

第一种场景与第二种场景有些相似,但却采用了相反的思路:当你想要为现有的 trait 添加一个类型参数来扩展功能时,你可以给它设定一个默认值来避免破坏已经实现的代码。

用于消除歧义的完全限定语法:调用相同名称的方法

Rust 既不会阻止两个 trait 拥有相同名称的方法,也不会阻止你为同一个类型实现这样的两个 trait。
你甚至可以在这个类型上直接实现与 trait 方法同名的方法。

当你调用这些同名方法时,你需要明确地告诉 Rust 你期望调用的具体对象。 思考示例中的代码,它定义了两个拥有同名方法 fly 的 trait: Pilot 和 Wizard,并为类型 Human 实现了这两个 trait,而 Human 本身也正好实现了 fly 方法。每个 fly 方法都执行了不同的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
trait Pilot {
    fn fly(&self);
}

trait Wirzard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wirzard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

当我们在 Human 的实例上调用 fly 时,编译器会默认调用直接实现在类型上的方法,

1
2
3
4
5
fn main() {
    let person = Human;
    person.fly();
}
// *waving arms furiously*

为了调用实现在 Pilot trait 或 Wizard trait 中的 fly 方法,我们需要使用更加显式的语法来指定具体的 fly 方法,如下所示:

1
2
3
4
5
6
7
8
9
fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wirzard::fly(&person);
    person.fly();
}
// This is your captain speaking.
// Up!
// *waving arms furiously*

在方法名的前面指定 trait 名称向 Rust 清晰地表明了我们想要调用哪个 fly 实现。
另外,你也可以使用类型的Human::fly(&person)语句,它与person.fly()在行为上等价,但会稍微冗长一些

当你拥有两种实现了同一个 trait 的类型,对于 fly 等需要接收 self 作为参数的方法,Rust 可以自动地根据 self 的类型推导出具体的 trait 实现。

然而,因为 trait 中的关联函数没有 self 参数,所以当在同一作用域下有两个实现了此种 trait 的类型时,Rust 无法推导出你究竟想要调用哪一个具体类型,除非使用完全限定语法。

例如,下例中的 Animal trait 拥有关联函数 baby_name,而示例中定义的 Dog 结构体在拥有独立关联函数 baby_name 的同时实现了 Animal trait。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
// A baby dog is called a Spot

使用这段代码的动物收容所希望将所有的小狗都叫做 Spot,他们在 Dog 的关联函数 baby_name 中实现了这一需求。
另外,Dog 类型还同时实现了用于描述动物的通用 trait:Animal。
Dog 在实现该 trait 的 baby_name 函数时将小狗称为 puppy。

我们希望的是调用在 Dog 上实现的 Animal trait 的 baby_name 函数来打印出 A baby dog is called a puppy。
我们将代码改成如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

2  |     fn baby_name() -> String;
   |     ------------------------- required by `Animal::baby_name`
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot infer type
   |
   = note: cannot satisfy `_: Animal`

由于 Animal:baby_name 是一个没有 self 参数的关联函数而不是方法,所以 Rust 无法推断出我们想要调用哪一个 Animal::baby_name 的实现。

为了消除歧义并指示 Rust 使用 Dog 为 Animal trait 实现的 baby_name 函数,我们需要使用完全限定语法。

1
2
3
4
fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
// A baby dog is called a puppy

这段代码在尖括号中提供的类型标注表明我们希望将 Dog 类型视作 Animal,并调用 Dog 为 Animal trait 实现的 baby_name 函数。

一般来说,完全限定语法被定义为如下所示的形式:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

对于关联函数而言,上面的形式会缺少 receiver 而只保留剩下的参数列表。
你可以在任何调用函数或方法的地方使用完全限定语法,而 Rust 允许你忽略那些能够从其他上下文信息中推导出来的部分。
只有当代码中存在多个同名实现,且 Rust 也无法区分出你期望调用哪个具体实现时,你才需要使用这种较为烦琐的显式语法。

用于在 trait 中附带另外一个 trait 功能的超 trait

有时,你会需要在一个 trait 中使用另外一个 trait 的功能。在这种情况下,我们需要使当前 trait 的功能依赖于另外一个同时被实现的 trait。
这个被依赖的 trait 也就是当前 trait 的超 trait(supertrait)

例如,假设我们希望创建一个拥有outline_print方法的 OutlinePrint trait,这个方法会在调用时打印出带有星号框的实例值。
换句话说,给定一个实现了 Display trait 的 Point 结构体,如果它会将自己的值显示为 (x, y),那么当 x 和 y 分别是 1 和 3 时,调用 outline_pring 就会打印出如下所示的内容:

1
2
3
4
5
**********
*      *
*(1, 3) *
*      *
**********

由于我们想要在 outline_print 的默认实现中使用 Display trait 的功能,所以 OutlinePrint trait 必须注明自己只能用于那些提供了 Display 功能的类型。
我们可以在定义 trait 时指定 OutlinePrint: Display 来完成该声明,这有些类似于为泛型添加 trait 约束。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("{}", "*".repeat(len + 2));
        println!("* {} *", output);
        println!("{}", "*".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}


struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}
// **********
// ********
// * (1, 3) *
// ********
// **********

使用 newtype 模式在外部类型上实现外部 trait