Rust for cpp dev - Trait 的高级用法

在之前的章节中,我们的代码中出现了一些 trait 的用法,但是并没有展开来讲。本章我们将集中介绍 trait 的所有高级用法。

Placeholder Type

再来看看之前的 Iterator 定义:

pub trait Iterator {
    type Item;

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

这里的 type Item 就是一个 placeholder type。它存在的意义是让这个 trait 的定义中的其他函数签名可以使用 Item 作为类型的 placeholder。而当实现 Iterator 这个 trait 时,用实际的类型去替换这个 placeholder。

impl Iterator for Counter {
    type Item = u32;

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

这听起来非常像泛型。那么为什么不写成下面的形式呢:

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

这主要是为了保证对于一个类型,只有一个 Iterator 的实现。如果用泛型,那么就可以实现多个,例如Iterator<u32>Iterator<String>。这样在使用的时候,也必须指明类型,对于 Iterator,我们显然只需要一种实现。

默认泛型参数和操作符重载

为泛型指定默认类型的语法是:

<PlaceholderType=ConcreteType>

一个经典用例是操作符重载。Rust 不允许任意创建或者重载操作符,只能通过重载 std::ops 中定义的 trait 来实现。例如,我们可以重载对 Point 的加操作:

use std::ops::Add;

// Debug for "{:?}", PartialEq for "=="
#[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 }
    );
}

上面代码中我们实现了 std::ops::Add trait:

trait Add<Rhs=Self> {
    type Output;

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

这个 trait 是含有一个默认参数Rhs的,使用它可以允许不同类型相加,这也是为什么它需要一个 placeholder type Output,否则返回值不知道应该选哪个类型了。

下面的代码实现了不同类型间的 Add

use std::ops::Add;

struct Meters(u32);

#[derive(Debug, PartialEq)]
struct Millimeters(u32);

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

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

fn main() {
    assert_eq!(Millimeters(500) + Meters(1), Millimeters(1500));
}

因此,默认参数的主要作用是:

  • 允许扩展一个类型,但是不破坏现有的代码
  • 允许用户自定义一些极少会用到的行为

调用不同 trait 的同名方法

Rust 允许不同的 trait 用同样的方法名。因此,对于同一个对象,如何区分调用哪个 trait 的方法呢?

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

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

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

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

fn main() {
    let person = Human;
    person.fly();
    // specify trait name before method to disambiguate
    Pilot::fly(&person);
    Wizard::fly(&person);
}

可以通过明确指出 trait 名区分:

    person.fly();
    // specify trait name before method to disambiguate
    Pilot::fly(&person);
    Wizard::fly(&person);

使用 supertrait 表明 trait 的依赖关系

有时,实现某个 trait A 需要使用另一个 trait B。这时候,trait B 就称为 supertrait。我们可以用下面形式表明他们的关系:

trait A: B {
    // snip

这样,如果某个类只实现了 A 但是没有实现 B,编译时就会报错。

使用 newtype pattern 实现 trait

回忆一下之前讲过的“孤儿准则”:

如果 type 和 trait 都不是在当前 crate 定义的,那不允许为这个 type 实现这个 trait。

实际上,我们可以通过 newtype pattern 绕过这个检查,即,将这个 type 封装在一个 tuple struct 里。

例如我们希望自己给 Vec<T> 类型实现 Display trait,根据孤儿准则,由于 Vec<T> 类型和 Display trait 都是在其他 crate 定义的,因此无法直接实现。我们就将 Vec<T> 包装成另一个类型绕过检查:

struct Wrapper(Vec<String>);

代码为:

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

newtype pattern 还能实现更多的 trait,例如 Deref,来直接返回内部的数据 &self.0

推荐阅读更多精彩内容