高级 Trait
我们在第 10 章的 “Trait:定义共享行为”部分首次介绍了 trait,但我们没有讨论更高级的细节。既然您对 Rust 有了更多了解,我们就可以深入探讨细节了。
使用关联类型在 Trait 定义中指定占位符类型
关联类型 将类型占位符与 trait 连接起来,这样 trait 方法定义就可以在其签名中使用这些占位符类型。trait 的实现者将指定具体的类型来代替特定实现的占位符类型。这样,我们就可以定义一个使用某些类型的 trait,而无需确切知道这些类型是什么,直到 trait 被实现。
在本章中,我们将大多数高级特性描述为很少需要。关联类型介于两者之间:它们的使用频率低于本书其余部分解释的特性,但高于本章讨论的许多其他特性。
标准库提供的具有关联类型的 trait 的一个示例是 Iterator
trait。关联类型名为 Item
,代表实现 Iterator
trait 的类型正在迭代的值的类型。Iterator
trait 的定义如列表 19-12 所示。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
列表 19-12:具有关联类型 Item
的 Iterator
trait 的定义
类型 Item
是一个占位符,next
方法的定义表明它将返回 Option<Self::Item>
类型的值。Iterator
trait 的实现者将为 Item
指定具体类型,next
方法将返回一个 Option
,其中包含该具体类型的值。
关联类型可能看起来类似于泛型的概念,因为后者允许我们定义一个函数而无需指定它可以处理哪些类型。为了检查这两个概念之间的差异,我们将查看在名为 Counter
的类型上实现 Iterator
trait 的示例,该示例指定 Item
类型为 u32
文件名:src/lib.rs
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
此语法似乎与泛型的语法相当。那么为什么不直接使用泛型定义 Iterator
trait,如列表 19-13 所示?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
列表 19-13:使用泛型的 Iterator
trait 的假设定义
区别在于,当使用泛型时,如列表 19-13 所示,我们必须在每个实现中注释类型;因为我们也可以为 Counter
实现 Iterator<String>
或任何其他类型,所以我们可以为 Counter
实现 Iterator
的多个版本。换句话说,当 trait 具有泛型参数时,它可以为一个类型实现多次,每次都更改泛型类型参数的具体类型。当我们在 Counter
上使用 next
方法时,我们将必须提供类型注释来指示我们要使用哪个 Iterator
实现。
使用关联类型,我们不需要注释类型,因为我们不能在一个类型上多次实现 trait。在列表 19-12 中,使用关联类型的定义,我们只能选择 Item
的类型一次,因为对于 Counter
只能有一个 impl Iterator
。我们不必在每次调用 Counter
上的 next
时都指定我们想要 u32
值的迭代器。
关联类型也成为 trait 契约的一部分:trait 的实现者必须提供一个类型来代替关联类型占位符。关联类型通常有一个名称来描述该类型将如何使用,并且在 API 文档中记录关联类型是一个好的做法。
默认泛型类型参数和运算符重载
当我们使用泛型类型参数时,我们可以为泛型类型指定默认的具体类型。如果默认类型有效,则无需 trait 的实现者指定具体类型。当使用 <PlaceholderType=ConcreteType>
语法声明泛型类型时,您可以指定默认类型。
这种技术的一个很好的用例是运算符重载,您可以在特定情况下自定义运算符(例如 +
)的行为。
Rust 不允许您创建自己的运算符或重载任意运算符。但是您可以通过实现与运算符关联的 trait 来重载 std::ops
中列出的操作和相应的 trait。例如,在列表 19-14 中,我们重载 +
运算符以将两个 Point
实例相加。我们通过在 Point
结构体上实现 Add
trait 来做到这一点
文件名:src/main.rs
use std::ops::Add; #[derive(Debug, Copy, Clone, 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 } ); }
列表 19-14:实现 Add
trait 以重载 Point
实例的 +
运算符
add
方法将两个 Point
实例的 x
值和两个 Point
实例的 y
值相加,以创建一个新的 Point
。Add
trait 具有一个名为 Output
的关联类型,该类型确定 add
方法返回的类型。
此代码中的默认泛型类型在 Add
trait 中。这是它的定义
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
此代码看起来应该很熟悉:一个具有一个方法和一个关联类型的 trait。新部分是 Rhs=Self
:此语法称为默认类型参数。Rhs
泛型类型参数(“right hand side” 的缩写)定义了 add
方法中 rhs
参数的类型。如果我们实现 Add
trait 时未指定 Rhs
的具体类型,则 Rhs
的类型将默认为 Self
,这将是我们正在实现 Add
的类型。
当我们为 Point
实现 Add
时,我们使用了 Rhs
的默认值,因为我们想要添加两个 Point
实例。让我们看一个实现 Add
trait 的示例,在该示例中,我们想要自定义 Rhs
类型,而不是使用默认值。
我们有两个结构体 Millimeters
和 Meters
,它们以不同的单位保存值。这种将现有类型薄包装在另一个结构体中的方式称为 newtype 模式,我们在 “使用 Newtype 模式在外部类型上实现外部 Trait”部分中更详细地描述了该模式。我们想要将毫米值添加到米值,并让 Add
的实现正确地进行转换。我们可以为 Millimeters
实现 Add
,并将 Meters
作为 Rhs
,如列表 19-15 所示。
文件名:src/lib.rs
use std::ops::Add;
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))
}
}
列表 19-15:在 Millimeters
上实现 Add
trait 以将 Millimeters
添加到 Meters
要添加 Millimeters
和 Meters
,我们指定 impl Add<Meters>
以设置 Rhs
类型参数的值,而不是使用默认值 Self
。
您将主要通过两种方式使用默认类型参数
- 在不破坏现有代码的情况下扩展类型
- 允许在大多数用户不需要的特定情况下进行自定义
标准库的 Add
trait 是第二个目的的示例:通常,您将添加两个相似的类型,但是 Add
trait 提供了自定义超出该范围的能力。在 Add
trait 定义中使用默认类型参数意味着您不必在大多数情况下都指定额外的参数。换句话说,不需要一些实现样板代码,从而使 trait 更易于使用。
第一个目的类似于第二个目的,但方向相反:如果您想向现有 trait 添加类型参数,则可以为其指定默认值,以便在不破坏现有实现代码的情况下扩展 trait 的功能。
消除歧义的完全限定语法:调用具有相同名称的方法
Rust 中没有任何东西阻止 trait 具有与另一个 trait 的方法名称相同的方法,Rust 也没有阻止您在一个类型上实现这两个 trait。也可以直接在该类型上实现与 trait 中的方法名称相同的方法。
当调用具有相同名称的方法时,您需要告诉 Rust 您要使用哪一个。考虑列表 19-16 中的代码,我们在其中定义了两个 trait Pilot
和 Wizard
,它们都具有一个名为 fly
的方法。然后,我们在类型 Human
上实现了这两个 trait,该类型已经有一个名为 fly
的方法直接在其上实现。每个 fly
方法的功能都不同。
文件名:src/main.rs
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() {}
列表 19-16:定义了两个 trait 具有 fly
方法,并在 Human
类型上实现了这些 trait,并且直接在 Human
上实现了 fly
方法
当我们在 Human
的实例上调用 fly
时,编译器默认调用直接在该类型上实现的方法,如列表 19-17 所示。
文件名:src/main.rs
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(); }
列表 19-17:在 Human
的实例上调用 fly
运行此代码将打印 *waving arms furiously*
,表明 Rust 调用了直接在 Human
上实现的 fly
方法。
要从 Pilot
trait 或 Wizard
trait 调用 fly
方法,我们需要使用更明确的语法来指定我们指的是哪个 fly
方法。列表 19-18 演示了此语法。
文件名:src/main.rs
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; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
列表 19-18:指定我们要调用的 trait 的 fly
方法
在方法名称之前指定 trait 名称向 Rust 明确表明了我们要调用的 fly
的哪个实现。我们也可以编写 Human::fly(&person)
,这等效于我们在列表 19-18 中使用的 person.fly()
,但是如果我们不需要消除歧义,则这种写法会稍微长一些。
运行此代码会打印以下内容
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
由于 fly
方法采用 self
参数,因此,如果我们有两个类型都实现了一个trait,则 Rust 可以根据 self
的类型来确定要使用哪个 trait 的实现。
但是,不是方法的关联函数没有 self
参数。当有多个类型或 trait 定义了具有相同函数名称的非方法函数时,除非您使用完全限定语法,否则 Rust 并不总是知道您指的是哪个类型。例如,在列表 19-19 中,我们为一个动物收容所创建了一个 trait,该收容所想要将所有幼犬命名为 Spot。我们创建了一个具有关联非方法函数 baby_name
的 Animal
trait。Animal
trait 是为结构体 Dog
实现的,我们在 Dog
上也直接提供了一个关联非方法函数 baby_name
。
文件名:src/main.rs
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()); }
列表 19-19:一个具有关联函数的 trait 和一个具有相同名称的关联函数的类型,该类型也实现了该 trait
我们在 Dog
上定义的 baby_name
关联函数中实现了将所有幼犬命名为 Spot 的代码。Dog
类型还实现了 trait Animal
,该 trait 描述了所有动物都具有的特征。幼犬被称为 puppies,这在 Animal
trait 在 Dog
上的实现中的与 Animal
trait 关联的 baby_name
函数中表达。
在 main
中,我们调用 Dog::baby_name
函数,该函数调用直接在 Dog
上定义的关联函数。此代码打印以下内容
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
此输出不是我们想要的。我们想要调用作为我们在 Dog
上实现的 Animal
trait 的一部分的 baby_name
函数,以便代码打印 A baby dog is called a puppy
。我们在列表 19-18 中使用的指定 trait 名称的技术在这里没有帮助;如果我们将 main
更改为列表 19-20 中的代码,我们将收到编译错误。
文件名:src/main.rs
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 {}", Animal::baby_name());
}
列表 19-20:尝试从 Animal
trait 调用 baby_name
函数,但 Rust 不知道要使用哪个实现
由于 Animal::baby_name
没有 self
参数,并且可能有其他类型实现了 Animal
trait,因此 Rust 无法确定我们想要哪个 Animal::baby_name
实现。我们将收到此编译器错误
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
为了消除歧义并告诉 Rust 我们想要使用 Animal
在 Dog
上的实现,而不是 Animal
在其他类型上的实现,我们需要使用完全限定语法。列表 19-21 演示了如何使用完全限定语法。
文件名:src/main.rs
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 as Animal>::baby_name()); }
列表 19-21:使用完全限定语法来指定我们要调用 Animal
trait 在 Dog
上实现的 baby_name
函数
我们在尖括号内提供了类型注释,这表明我们想要调用 Animal
trait 在 Dog
上实现的 baby_name
函数,方法是说我们希望将 Dog
类型视为此函数调用的 Animal
。现在,此代码将打印我们想要的内容
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
通常,完全限定语法定义如下
<Type as Trait>::function(receiver_if_method, next_arg, ...);
对于不是方法的关联函数,将没有 receiver
:只会存在其他参数的列表。您可以在调用函数或方法的任何地方使用完全限定语法。但是,您可以省略 Rust 可以从程序中的其他信息中推断出的任何部分语法。您只需要在有多个实现使用相同名称并且 Rust 需要帮助来识别您要调用的实现的情况下使用这种更冗长的语法。
使用 Supertrait 在另一个 Trait 中要求一个 Trait 的功能
有时,您可能会编写一个依赖于另一个 trait 的 trait 定义:为了使类型实现第一个 trait,您希望要求该类型也实现第二个 trait。您这样做是为了使您的 trait 定义可以使用第二个 trait 的关联项。您的 trait 定义所依赖的 trait 称为您的 trait 的supertrait。
例如,假设我们要创建一个带有 outline_print
方法的 OutlinePrint
trait,该方法将打印一个给定的值,格式化为用星号框起来。也就是说,给定一个实现了标准库 trait Display
以产生 (x, y)
的 Point
结构体,当我们在 x 为 1
且 y 为 3
的 Point
实例上调用 outline_print
时,它应打印以下内容
**********
* *
* (1, 3) *
* *
**********
在 outline_print
方法的实现中,我们想要使用 Display
trait 的功能。因此,我们需要指定 OutlinePrint
trait 仅适用于也实现 Display
并提供 OutlinePrint
所需功能的类型。我们可以在 trait 定义中通过指定 OutlinePrint: Display
来做到这一点。这种技术类似于向 trait 添加 trait 约束。列表 19-22 显示了 OutlinePrint
trait 的实现。
文件名:src/main.rs
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)); } } fn main() {}
列表 19-22:实现需要 Display
功能的 OutlinePrint
trait
由于我们已指定 OutlinePrint
需要 Display
trait,因此我们可以使用为任何实现 Display
的类型自动实现的 to_string
函数。如果我们尝试使用 to_string
而不添加冒号并在 trait 名称后指定 Display
trait,则会收到错误消息,指出在当前作用域中找不到类型 &Self
的名为 to_string
的方法。
让我们看看当我们尝试在未实现 Display
的类型(例如 Point
结构体)上实现 OutlinePrint
时会发生什么
文件名:src/main.rs
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 OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
我们收到一个错误,指出需要 Display
但未实现
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
要解决此问题,我们在 Point
上实现 Display
并满足 OutlinePrint
要求的约束,如下所示
文件名:src/main.rs
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 OutlinePrint for Point {} use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3 }; p.outline_print(); }
然后,在 Point
上实现 OutlinePrint
trait 将成功编译,并且我们可以调用 Point
实例上的 outline_print
以在星号轮廓内显示它。
使用 Newtype 模式在外部类型上实现外部 Trait
在第 10 章的 “在类型上实现 Trait”部分中,我们提到了孤儿规则,该规则规定,仅当 trait 或类型是本地 crate 时,我们才允许在类型上实现 trait。可以使用 newtype 模式 来规避此限制,该模式涉及在元组结构体中创建新类型。(我们在第 5 章的 “使用没有命名字段的元组结构体来创建不同类型”部分中介绍了元组结构体。)元组结构体将具有一个字段,并且是我们要为其实现 trait 的类型的薄包装器。然后,包装器类型是本地 crate 的,我们可以在包装器上实现 trait。Newtype 是一个起源于 Haskell 编程语言的术语。使用此模式不会产生运行时性能损失,并且包装器类型在编译时会被省略。
例如,假设我们要为 Vec<T>
实现 Display
,孤儿规则阻止我们直接这样做,因为 Display
trait 和 Vec<T>
类型是在我们的 crate 外部定义的。我们可以创建一个 Wrapper
结构体来保存 Vec<T>
的实例;然后我们可以在 Wrapper
上实现 Display
并使用 Vec<T>
值,如列表 19-23 所示。
文件名:src/main.rs
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}"); }
列表 19-23:围绕 Vec<String>
创建 Wrapper
类型以实现 Display
Display
的实现使用 self.0
来访问内部 Vec<T>
,因为 Wrapper
是一个元组结构体,而 Vec<T>
是元组中索引 0 处的项。然后,我们可以在 Wrapper
上使用 Display
trait 的功能。
使用此技术的缺点是 Wrapper
是一种新类型,因此它不具有其所持有的值的方法。我们将必须在 Wrapper
上直接实现 Vec<T>
的所有方法,以便这些方法委托给 self.0
,这将使我们能够将 Wrapper
完全像 Vec<T>
一样对待。如果我们希望新类型具有内部类型的所有方法,则在 Wrapper
上实现 Deref
trait(在第 15 章的 “使用 Deref
Trait 将智能指针视为常规引用”部分中讨论)以返回内部类型将是一种解决方案。如果我们不希望 Wrapper
类型具有内部类型的所有方法(例如,限制 Wrapper
类型的行为),我们将必须手动实现我们想要的方法。
即使不涉及 trait,这种 newtype 模式也很有用。让我们转移焦点,看看与 Rust 类型系统交互的一些高级方法。