Skip to content

5.5 trait 约束和继承

Rust 的 trait 的另外一个大用处是,作为泛型约束使用。关于泛型,本书第三部分还会详细解释。

5.5.1 trait 作为参数

我们现在对 trait 的定义及其实现已经有了初步的了解,下面我们看看如何利用 trait 来接受不同类型的参数。譬如,定义一个函数my_print来格式化打印。

rust
use std::fmt::Debug;

fn my_print(x: impl Debug) {
    println!("The value is {:?}.", x);
}

fn main() {
    my_print("China");
    my_print(41_i32);
    my_print(true);
    my_print(['a', 'b', 'c'])
}

对于参数 x,我们使用 impl + trait(impl Debug)对其类型进行了约束,它要求x参数的类型必须实现了 Debug trait。

这是因为我们在函数体内,用到了 println! 格式化打印,而且用了 {:?} 这样的格式控制符,它要求类型满足 Debug 的约束,否则编译不过。

在调用的时候,凡是满足 Debug 约束的类型都可以是这个函数的参数,所以我们可以看到以上四种调用都是可以编译通过的。 假如我们自定义一个类型,而它没有实现 Debug trait,用这个类型作为 my_print 的参数的话,编译就会报错。

所以,泛型约束既是对实现部分的约束,也是对调用部分的约束。

5.5.2 Trait Bounds 语法

上面例子中 impl + trait 名称的写法可以看做是 trait bounds 的语法糖,参考下面的例子:

rust
pub trait Summary {
    fn summarize(&self) -> String;
}

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

impl + trait 名称的语法很方便,适用于参数比较简单的场景。trait bounds 则适用于更复杂的场景。 譬如,当notify函数有2个参数,并且都是实现了 Summary trait 的类型,使用 impl + trait 名称的定义如下:

rust
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

在上面的定义中 item1item2 的具体类型可以是不同的,也可以是相同的,没有限制。 如果我们希望限制它们是相同类型,使用 impl + trait 名称就无法做到了,但是 trait bounds 可以:

rust
pub fn notify<T: Summary>(item1: T, item2: T) {}

我们还可以为参数指定多个 trait bounds,即类型的定义里需要实现相应的多个 trait,假设我们指定了两个类型,Display 和 Summary:

rust
pub fn notify(item: &(impl Summary + Display)) {
 // --snip--
}
rust
pub fn notify<T: Summary + Display>(item: &T) {
 // --snip--
}

上面两种写法都可以,区别前面已经做过介绍。

5.5.3 通过 where 提高可读性

在 trait bounds 比较多的场景下,每个泛型有其自己的 trait bounds,上面两种写法都会变得难以阅读。为此,Rust 提供了where从句来指定 trait bounds 的语法。

示例如下:

rust
fn my_print<T>(x: T) where T: Debug {
    println!("The value is {:?}.", x);
}

对于这种简单的情况,两种写法都可以。但是在某些复杂的情况下,泛型约束只有where从句可以表达,泛型参数后面直接加冒号的写法表达不出来,比如涉及关联类型的时候,请参见第 21 章

trait 允许继承。类似下面这样:

rust
trait Base { ... }
trait Derived : Base { ... }

这表示 Derived trait 继承了 Base trait。它表达的意思是,满足 Derived 的类型,必然也满足 Base trait。 所以,我们在针对一个具体类型 impl Derived的时候,编译器也会要求我们同时 impl Base。示例如下:

rust
trait Base {}

trait Derived : Base {}

struct T;

impl Derived for T {}

fn main() {
}

编译,出现错误,提示信息为:

rust
7 | impl Derived for T {}
  |      ^^^^^^^ the trait `Base` is not implemented for `T`

我们再加上一句

rust
impl Base for T {}

编译器就不再报错了。

实际上,在编译器的眼中,trait Derived: Base{}等同于 trait Derived where Self: Base{}。 这两种写法没有本质上的区别,都是给 Derived 这个 trait 加了一个约束条件,即实现 Derived trait 的具体类型,也必须满足 Base trait 的约束。

在标准库中,很多 trait 之间都有继承关系,比如:

rust
trait Eq: PartialEq<Self> {}
trait Copy: Clone {}
trait Ord: Eq + PartialOrd<Self> {}
trait FnMut<Args>: FnOnce<Args> {}
trait Fn<Args>: FnMut<Args> {}

读完本书后,读者应该能够理解这些 trait 是用来做什么的,以及为什么这些 trait 之间会有这样的继承关系。

5.5.4 通过 Trait Bounds 有条件地实现方法

rust
use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

上面代码第一个 impl 块中 Pair<T> 类型实现了 new 函数来返回 Pair<T> 的新实例(Self 是 impl 块类型的类型别名,在本例中是 Pair<T> )。但是在下一个 impl 块中,Pair<T> 被约束为只有在其内部类型 T 实现了支持比较的 PartialOrd trait 和支持打印的 display trait 时才可以实现 cmp_display 方法。

满足 trait bounds 的任何类型上的 trait 的实现被称为一揽子实现(blanket implementations),在 Rust 标准库中被广泛使用。例如,标准库在任何实现 Display trait 的类型上实现 ToString trait。标准库中的 impl 块看起来与此代码类似:

rust
impl<T: Display> ToString for T {
    // --snip--
}

Released under the MIT License