参考章节《Rust程序设计语言》第19.2章 高级 trait
本节我们将学习 trait
的一些高级用法,它们包括
- 关联类型
- 默认泛型类型参数
- 运算符重载
- 完全限定语法
<Type as Trait>::function(receiver_if_method, next_arg, ...);
- 在
trait
中需要用到另一个 trait
的某个功能,说白了需要能够依赖相关的 trait
也被实现。
- newtype
模式(注意是模式,不是关键字)
用于在外部crate
上实现 外部trait
关联类型
关联类型是一个将类型占位符与 trait
相关联的方式,这样 trait
的方法签名中就可以使用这些占位符类型。
trait
的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。
一个带有关联类型的 trait
的例子是标准库提供的 Iterator trait
。它有一个叫做 Item
的关联类型来替代遍历的值的类型。
1
2
3
4
5
|
pub trait Iterator {
type Item; // Item 是一个占位类型,这个 trait 的实现者会指定 Item 的具体类型
fn next(&mut self) -> Option<Self::Item>; // next 方法定义表明它返回 Option<Self::Item> 类型的值。
}
|
在实现时,当实现者指定了关联类型时,方法的返回值就是其指定的类型
1
2
3
4
5
6
7
|
impl Iterator for Counter {
type Item = u32; // 指定关联类型为 u32
fn next(&mut self) -> Option<Self::Item> {
// --省略部分代码--
}
}
|
这类似于泛型
,那为什么不这么定义呢?
1
2
3
|
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
|
它和泛型有什么区别呢?感兴趣可以参考 Rust程序设计语言 示例 19-13: 一个使用泛型的 Iterator trait 假想定义
简单总结一下,关联类型
,通常用于指定 trait 方法的返回值类型
默认泛型类型参数
当使用泛型类型参数时,可以为泛型指定一个默认
的具体类型。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <类型占位符名称=实际的类型>
。
1
2
3
4
5
6
7
|
// 定义泛型类型默认参数 Rhs=Self
// 如果实现 Add trait 时不指定 Rhs 的具体类型,Rhs 的类型将是默认的 Self 类型
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
|
运算符重载
运算符重载是指在特定情况下自定义运算符(比如 +)
行为的操作。 例如,我们想通过 +
运算符让两个 Point
结构体相加生成一个新的Point
上面的 Add trait
就是一个用于实现运算符重载的 trait
,它可以重新定义特定情况下 +
运算符的行为,它位于 std::ops
包中
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
|
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// 实现 Add trait,实现时没有指定Rhs的具体类型,因此它是Self,因为我们希望将两个 Point 实例相加。
impl Add for Point {
type Output = Point; // 指定关联类型为 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 }, // 实现了 Add trait 后,就可以两个 Point 相加
Point { x: 3, y: 3 }
);
}
|
让我们看看一个实现 Add trait
时希望自定义 Rhs
类型而不是使用默认类型的例子。
下面这段程序,实现了 Millimeters
类型(代表毫米值) 和 Meters
类型(代表米值)相加,并让 Add
的实现正确处理转换。
你不用管什么米值毫米值什么意思,因为这不重要,重要的是要知道如何自定义Rhs
1
2
3
4
5
6
7
8
9
10
11
12
13
|
use std::ops::Add;
struct Millimeters(u32); // 这种将现有类型简单封装进另一个结构体的方式被称为 newtype 模式,这个后面再说,现在不用管
struct Meters(u32); // 这种将现有类型简单封装进另一个结构体的方式被称为 newtype 模式,这个后面再说,现在不用管
// 实现 Add trait, 为 Rhs 提供类型值 Meters
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
|
为了使 Millimeters
和 Meters
能够相加,我们指定 impl Add<Meters>
来设定 Rhs
类型参数的值而不是使用默认的 Self
。
完全限定语法
完全限定语法用于消除歧义
,我认为应该尽量避免写出有歧义
的代码,不过有时,可能实在没有更好的办法只能这样,我们来看看下面的代码
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
|
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Human {
// Human 自己的 fly 方法
fn fly(&self) {
println!("*waving arms furiously*");
}
}
impl Pilot for Human {
// 实现了 Pilot trait 的 flay 方法
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
// 实现了 Wizard trait 的 flay 方法
fn fly(&self) {
println!("Up!");
}
}
|
调用时,默认执行的是 Human
自己的 fly
方法
运行这段代码会打印出 *waving arms furiously*
,这表明 Rust 调用了直接实现在 Human
上的 fly
方法。
1
2
3
4
|
fn main() {
let person = Human;
person.fly();
}
|
为了能够调用 Pilot trait
或 Wizard trait
的 fly
方法,我们需要使用更明显的语法以便能指定我们指的是哪个 fly
方法。
1
2
3
4
5
6
|
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly(); // 等同于 Human::fly(&person)
}
|
运行这段代码会打印出
1
2
3
4
5
6
7
|
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
|
因为 fly
方法获取一个 self
参数,而 方法
第一个参数就是 self
,Rust 可以根据 self
的类型计算出应该使用哪一个 trait
实现。
但我们知道 关联函数没有self参数
,我们来看看下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
trait Animal {
fn baby_name() -> String; // 关联函数
}
struct Dog;
impl Dog {
// Dog 自己的关联函数
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
// 实现 Animal 的关联函数
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
|
运行这段代码会打印出
1
2
3
4
5
|
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
|
可以看到它直接调用了定义于 Dog
之上的关联函数。那如果我们想调用 Animal
上的 baby_name
呢
我们可以使用完全限定语法
,它的语法是
1
|
<Type as Trait>::function(receiver_if_method, next_arg, ...);
|
对于关联函数,其没有一个 receiver
,故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。
然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。
只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。
我们在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 Dog
类型当作 Animal
对待,指定希望调用的是 Dog
上 Animal trait
实现中的 baby_name
。
1
2
3
|
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
|
运行这段代码
1
2
3
4
5
|
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
|
在 trait 中需要用到另一个 trait 的某个功能
有时我们可能会需要某个 trait
使用另一个 trait
的功能。在这种情况下,需要能够依赖相关的 trait
也被实现。
例如我们希望创建一个带有 outline_print
方法的 trait OutlinePrint
,它会打印出带有星号框的值。
也就是说,如果 Point
实现了 Display
并返回 (x, y)
,调用以 1
作为 x
和 3
作为 y
的 Point
实例的 outline_print
会显示如下:
1
2
3
4
5
|
**********
* *
* (1, 3) *
* *
**********
|
可以通过在 trait
定义中指定 OutlinePrint: Display
来做到这一点。如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
use std::fmt;
// 指定 OutlinePrint 依赖 fmt::Display ,这类似 trait bounds
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string(); // 因为指定了 OutlinePrint 需要 Display trait,则可以在 outline_print 中使用 to_string
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
|
如果尝试在一个没有实现 Display
的类型上实现 OutlinePrint
则会得到一个错误说 Display
是必须的而未被实现。
newtype 模式用于在外部crate 上实现外部 trait
这里,书上说了一大堆废话,我是没看懂什么意思,这里直接摘录重要部分
例如,如果想要在 Vec<T>
上实现 Display
,而孤儿规则阻止我们直接这么做,因为 Display trait
和 Vec<T>
都定义于我们的 crate
之外。
可以创建一个包含 Vec<T>
实例的 Wrapper
结构体,接着可以在 Wrapper
上实现 Display
并使用 Vec<T>
的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
use std::fmt;
struct Wrapper(Vec<String>); // newtype 模式
impl fmt::Display for Wrapper {
// Display trait 只有一个方法就是 fmt
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", ")) // Display 的实现使用 self.0 来访问其内部的 Vec<T>,因为 Wrapper 是元组结构体而 Vec<T> 是结构体总位于索引 0 的项。
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
|
运行这段代码
1
2
3
4
5
|
Blocking waiting for file lock on build directory
Compiling rust_test v0.1.0 (/home/w/data/code/rust_test)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `/home/w/data/code/rust_test/target/debug/rust_test`
w = [hello, world]
|
此方法的缺点是,因为 Wrapper
是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper
上实现 Vec<T>
的所有方法
这样就可以代理到 self.0
上,这就允许我们完全像 Vec<T>
那样对待 Wrapper
。
如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref trait
并返回其内部类型是一种解决方案。
如果不希望封装类型拥有所有内部类型的方法,比如为了限制封装类型的行为,则必须只自行实现所需的方法。