Appearance
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) {}
在上面的定义中 item1
和 item2
的具体类型可以是不同的,也可以是相同的,没有限制。 如果我们希望限制它们是相同类型,使用 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--
}