rust-lang-zh_CN/src/advanced_features/adv_traits.md

478 lines
24 KiB
Markdown
Raw Normal View History

2023-12-01 13:34:21 +08:00
# 高级特质
在第 10 章 [“特质:定义共用行为”](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质定义共用行为) 小节中,咱们曾首先涉及到特质,但咱们不曾讨论更为高级的那些细节。现在咱们对 Rust 有了更多了解咱们就可以深入本质get into the nitty-gritty。
## 使用关联类型指定出特质定义中的一些占位性类型
**Specifying placeholder types in trait definitions with associated types**
*关联类型* 将类型占位符与特质加以结合,从而那些特质方法的定义,就可以在他们的签名中,使用这些占位符类型。特质的实现者,将为其特定实现,指明占位符类型所要使用的具体类型。如此一来,咱们便可以在特质被实现之前,无需准确获悉特质用到的类型下,定义出用到这些类型的特质。
在本章中,咱们已经介绍了绝大多数极少需要用到的高级特性。而关联类型则是位于这些高级特性中部的一种:其相较本书其余部分降到的那些特性,用得尤其少见,但相较这一章中讨论到的其他特性,则其要更常用一些。
带有关联类型特质的一个示例,便是标准库所提供的 `Iterator` 特质。其中的关联类型名为 `Item`,且代表着实现了这个 `Iterator` 特质的类型所迭代的那些值的类型。`Iterator` 特质的定义如下清单 19-12 中所示。
```rust
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
```
*清单 19-12有着关联类型 `Item``Iterator` 特质的定义*
其中的类型 `Item` 便是个占位符,而那个 `next` 方法的定义,则显示其将返回类型类型为 `Option<Self::Item>` 的值。`Iterator` 的实现者,将指明 `Item` 的具体类型,同时 `next` 方法将返回包含那个具体类型值的一个 `Option`
从泛型允许咱们在不指明函数可处理何种类型下,而定义出某个函数上看,关联类型可能看起来是个与泛型类似类似的概念。为检视这两个概念的不同,咱们将看看在指定了 `Item` 类型为 `u32` 的一个名为 `Counter` 的类型上的 `Iterator` 实现:
```rust
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --跳过代码--
```
这种语法似乎可与泛型的那种语法相比。那么为何没有只使用泛型定义 `Iterator`,如下清单 19-13 中所示的那样呢?
```rust
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
```
*清单 19-13使用泛型的一种 `Iterator` 特质的定义假设*
不同之处在于,如同清单 19-13 中使用泛型时,咱们必须注解每个实现中的那些类型;由于咱们还可以实现 `Iterfator<String> for Counter` 或任何其他类型,咱们可以有对 `Counter` 的多个 `Iterator` 实现。也就是说,在特质有着泛型参数时,他就可以对某个类型被实现多次,每次都修改泛型参数的具体类型。当在 `Counter` 上使用 `next` 方法时,咱们就将不得不提供类型注解,来表明咱们想要使用哪个 `Iterator` 实现。
而在关联类型下,由于我们无法在一个类型上多次实现某个特质,因此就无需注解类型。在上面有着用到关联类型定义的清单 9-12 中,由于只能有一个 `impl Iterator for Counter`,所以咱们就只能就`Item` 为何选择一次。咱们不必在 `Counter` 上调用 `next` 的每个地方,指定咱们所要的是个 `u32` 值的迭代器。
关联类型还成了特质合约的一部分:特质的实现着必须提供一种类型,来顶替那个关联类型占位符。关联类型通常会有个描述该类型将被如何使用的名字,而在 API 文档中对关联类型编写文档,则是良好的做法。
## 默认泛型参数与运算符的重载
**Default Generic Type Parameters and Operator Overloading**
> **注**:请参考 [Difference Between Method Overloading and Method Overriding in Java](https://www.geeksforgeeks.org/difference-between-method-overloading-and-method-overriding-in-java/) 了解 Java 中的重载与重写区别。
在咱们用到泛型参数时,咱们可以给泛型指定默认具体类型。在所指定的默认类型就有效时,这样做消除了实现者指定具体类型的需求。在声明泛型时使用 `<PlaceholderType=ConcreteType>` 语法,指定出默认类型。
这种技巧有用处情形的一个了不起示例,便是 *运算符重载operator overloading*,咱们可以其在某些情形下,定制某个运算符(比如 `+`)的行为。
Rust 不允许咱们创建自己的运算符,或重载任意运算符。但咱们可以通过实现与运算符相关的特质,而重载那些运算及于 `std::ops` 中所列出的相应特质。比如,在下面清单 19-14 中,咱们就将 `+` 运算符过载为把两个 `Point` 实例加在一起。咱们是通过在 `Point` 结构体上实现 `Add` 特质完成这一点的。
文件名:`src/main.rs`
```rust
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` 特质来为 `Point` 实例过载 `+` 运算符*
这里的 `add` 方法,将两个 `Point` 实例的 `x` 值及两个实例的 `y` 值相加,而创建出一个新的 `Point`。这里的 `Add` 特质有着一个确定自其中的 `add` 方法返回类型,名为的 `Output` 关联类型。
此代码中的默认泛型,是在 `Add` 特质里。以下便是其定义:
```rust
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
```
此代码看起来应相当熟悉:有着一个方法与关联类型的特质。其中新的部分为 `Rhs=Self`:这种语法叫做 *默认类型参数default type parameters*。其中的 `Rhs` 泛型参数(是 `right hand side` 的缩写),定义了 `add` 方法中 `rhs` 参数的类型,当咱们实现这个 `Add` 特质,而没有指明 `Rhs` 的类型时,`Rhs` 的类型将默认为 `Self`,其将是咱们在其上实现 `Add` 的类型。
当咱们为 `Point` 实现 `Add` 时,由于咱们打算把两个 `Point` 实例相加,因此而使用了 `Rhs` 的默认值。接下来看看,其中咱们打算定制那个 `Rhs` 而非使用其默认值的一个 `Add` 实现示例。
咱们有着两个结构体,`Millimeters` 与 `Meters`保存着不同单位的一些值。这种将某个既有类型封装在另一结构体的瘦封装thin wrapping就叫做 *新类型模式newtype pattern*,在后面的 [“使用新型模式在外部类型上实现外部特质”](#使用新型模式在外层类型上实现外层的特质) 小节,咱们会对其进行更深入讨论。咱们打算把毫米值与以米计数的值相加,并要让 `Add` 的实现,正确完成单位转换。咱们可在将 `Meters` 作为 `Rhs` 下,对 `Millimeters` 实现 `Add`,如下清单 19-15 中所示。
```rust
#[derive(Debug, Copy, Clone, PartialEq)]
struct Millimeters(u32);
#[derive(Debug, Copy, Clone, PartialEq)]
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` 特质,以将 `Millimeters``Meters` 相加*
为了将 `Millimeters``Meters` 相加,咱们指明了 `impl Add<Meters>` 来设置那个 `Rhs` 类型参数,而非使用其默认的 `Self`
咱们将以如下两种主要方式,使用默认的类型参数:
- 在不破坏既有代码之下,扩展某个类型;
- 为实现绝大多数不会需要的特定情形下的定制to allow customization in specific cases most users won't need。
标准库的 `Add` 特质,便是第二种目的的一个示例:通常,咱们将把两个相似类型相加,但 `Add` 特质提供了定制超出那种情况的能力。在 `Add` 特质中使用默认类型,就意味着咱们不必在多数时候指定额外的参数。换句话说,并不需要一点点的实现样板,从而令到使用这个特质更为容易。
第一个目的与第二个类似,不过是反过来的:在咱们打算将类型参数添加到某个既有特质时,就可以给到其一个默认值,从而在不破坏既有那些实现代码下,实现该特质功能的扩展。
## 用于消除歧义的完全合格语法:以同一名字调用方法
**Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name**
Rust 中没有什么可以阻止某个特质有着与另一特质的方法同样名字的方法Rust 也不会阻止咱们在一个类型上实现这两种特质。至于直接在类型上,以来自不同特质方法的同样名字实现方法,也是可行的。
在以同一名字调用这些方法时,咱们将需要告诉 Rust 打算使用哪一个。设想下面清单 19-16 中,定义了两个特质,`Pilot` 与 `Wizard`,两个特质都有一个叫做 `fly` 的代码。咱们随后在已在其上实现了一个名为 `fly` 方法的类型 `Human` 上,实现了这两个特质。每个 `fly` 都完成不同的事情。
```rust
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println! ("机长在此发言。");
}
}
impl Wizard for Human {
fn fly(&self) {
println! ("飞起来!");
}
}
impl Human {
fn fly(&self) {
println! ("*愤怒地挥动双臂*");
}
}
```
*清单 19-16两个被定义作有 `fly` 方法的特质并都在 `Human` 类型上被实现,且在 `Human` 上直接实现了一个 `fly` 方法*
当咱们在 `Human` 实例上调用 `fly` 时,编译器默认为调用直接在该类型上实现的那个方法,如下清单 19-17 中所示。
```rust
fn main() {
let person = Human;
person.fly();
}
```
*清单 19-17调用 `Human` 实例上的 `fly`*
运行此代码将打印出 `*愤怒地挥动双臂*`,显示 Rust 调用了直接在 `Human` 上实现的那个 `fly` 方法。
为了调用 `Pilot``Wizard` 特质上的 `fly` 方法,咱们需要使用更为显式的语法,来指明我们所指的是那个 `fly` 方法。下面清单 19-18 对此语法进行了演示。
文件名:`src/main.rs`
```rust
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
```
*清单 19-18指明咱们打算调用哪个特质的 `fly` 方法*
在方法名字前指明特质名字,就向 Rust 澄清了咱们打算调用 `fly` 的哪个实现。咱们本来也可以写下 `Human::fly(&person)`,这与咱们曾在清单 19-18 中所使用的 `person.fly()` 等级,但若咱们无需消除歧义,这样写起来就些许有些长了。
运行此代码会打印以下输出:
```console
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/disambiguation`
机长在此发言。
飞起来!
*愤怒地挥动双臂*
```
由于 `fly` 方法取了一个 `self` 参数,那么当咱们有着实现了一个 *特质* 的两个 *类型*Rust 就可以根据 `self` 的类型,找出要使用特质的哪个实现。
然而,不是方法的那些关联函数,是没有 `self` 参数的。当存在以同样函数名字,定义了非方法函数的类型或特质时,除非咱们使用了 *完全合格语法fully qualified syntax*,否则 Rust 就不会总是清楚咱们所指的是何种类型。比如,在下面清单 19-19 中,咱们创建了一个用于动物收容所的特质,其中打算将所有狗崽都命名为 `点点`。咱们构造了带有关联的非方法函数 `baby_name` 的一个 `Animal` 特质。对结构体 `Dog` 实现了这个 `Animal` 特质,在 `Dog` 上咱们还直接提供了一个关联的非方法函数 `baby_name`
```rust
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("点点")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("Puppy")
}
}
fn main() {
println! ("狗崽叫做 {}", Dog::baby_name());
}
```
*清单 19-19有着一个关联函数的特质以及一个有着同样函数名字关联函数、还实现了那个特质的类型*
咱们是在那个定义在 `Dog` 上的关联函数里,实现的将全部狗仔命名为点点的代码。`Dog` 类型还实现了特质 `Animal`,该特质描述了全部动物都有的特征。小狗都叫做狗崽,且这一点是在 `Dog` 上的 `Animal` 特质中,与 `Animal` 特质关联的 `baby_name` 函数中得以表达的。
`main` 函数中,咱们调用了那个 `Dog::baby_name` 函数,这就会调用直接定义在 `Dog` 上的那个关联函数。此代码会打印下面的输出:
```console
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/disambiguation`
狗崽叫做 点点
```
此输出不是咱们想要的。咱们想要调用作为咱们曾在 `Dog` 上实现过的 `Animal` 特质一部分的那个 `baby_name` 函数,从而代码会打印出 `小狗叫做 狗崽`。咱们曾在清单 19-18 中用到的指定特质名字的技巧,这里就不管用了;而若咱们将 `main` 修改为下面清单 19-20 中的代码,咱们就将收到一个编译报错。
```rust
fn main() {
println! ("小狗叫做 {}", Animal::baby_name());
}
```
*清单 19-20尝试调用 `Animal` 特质中的那个 `baby_name` 函数,但 Rust 不清楚要使用那个实现*
由于 `Animal::baby_name` 没有 `self` 参数,且这里可能有别的实现了 `Animal` 特质的类型,因此 Rust 就无法计算出咱们想要的那个 `Animal::baby_name` 实现。咱们将得到下面这个编译器错误:
```console
$ cargo run
Compiling disambiguation v0.1.0 (/home/lenny.peng/rust-lang/disambiguation)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:26
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println! ("小狗叫做 {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println! ("小狗叫做 {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `disambiguation` due to previous error
```
为消除歧义并告知 Rust 咱们打算使用 `Dog` 的那个 `Animal` 实现,而非某种其他类型的 `Animal` 实现,咱们需要使用完全合格语法。下面清单 19-21 演示了怎样使用完全合格语法。
文件名:`src/main.rs`
```rust
fn main() {
println! ("小狗叫做 {}", <Dog as Animal>::baby_name());
}
```
*清单 19-21使用完全合格语法来指明咱们是要调用实现在 `Dog` 上的 `Animal` 特质中的那个 `baby_name` 函数*
通过讲出咱们希望将 `Dog` 类型,针对这个 `baby_name` 函数调用而作为 `Animal` 对待,从而表明咱们打算调用实现在 `Dog` 上的 `Animal` 特质中的 `baby_name` 方法,这样位处那尖括号中的类型注解,提供给 Rust。此代码现在将打印出咱们想要的输出
```console
$ cargo run
Compiling disambiguation v0.1.0 (/home/lenny.peng/rust-lang/disambiguation)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/disambiguation`
小狗叫做 狗崽
```
一般来讲,完全合格语法是像下面这样定义的:
```rust
<Type as Trait>::function(receiver_if_method, next_arg, ...);
```
对于那些不是方法的语法,此处就不会有 `receiver`:这里将只有其他参数的清单。在调用函数或方法的所有地方,咱们都可以使用完全合格语法。不过,在 Rust 能够从程序中另外的信息计算出(要调用哪个函数或方法)时,那么这种语法便是可以省略的。咱们只需在有着多个使用了同一名字的实现,且 Rust 需要帮助来识别出咱们打算调用哪个实现时,才需要使用这种更为冗长的语法。
## 在一个特质里运用超特质寻求另一特质的功能
**Using Supertraits to Require One Trait's Functionality Within Another Trait**
有的时候,咱们可能会编写依赖于另一特质的特质:对于要实现前一个特质的类型,咱们希望寻求那个类型也实现后一个特质。为了咱们的特质定义,可以利用后一个特质的那些关联项目,咱们就会实现这一点。咱们的特质所依赖的那个特质,被称为咱们特质的 *超特质supertrait*
比方说,咱们打算构造一个带有将所给的值格式化,从而其被星号框起来的 `outline_print` 方法,这样一个 `OutlinePrint` 特质。而那个所给的值则是,一个实现了标准库特质 `Display` 来得到 `(x, y)``Point` 结构体,即当咱们在有着 `x``1` `y``3``Point` 上调用 `outline_print` 时,其将打印以下输出:
```console
**********
* *
* (1, 3) *
* *
**********
```
`outline_print` 方法的实现中,咱们打算使用 `Display` 特质的功能。因此,咱们就需要指明,这个 `OutlinePrint` 特质将只对那些同时实现了 `Display` 生效,且提供了 `OutlinePrint` 所需的功能。咱们可以通过指明 `OutlinePrint: Display`,在该特质定义中实现那一点。这种技巧类似于给特质添加特质边界。下面清单 19-22 给出了这个 `OutlinePrint` 特质的一种实现。
```rust
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));
}
}
```
*清单 19-22需要 `Display` 中功能的 `OutlinePrint` 特质实现*
由于咱们已指明 `OutlinePrint` 需要 `Display` 特质,因此咱们就可以使用那个任何实现了 `Display` 类型上均已实现了的 `to_string` 函数。若咱们在没有于特质名字之后加上冒号并指明 `Display` 特质,便尝试使用 `to_string`,咱们就会得到一个声称当前作用域中的类型 `&Self` 下,未找到名为 `to_string` 的方法的报错。
下面来看看当咱们尝试在某个未实现 `Display` 的类型,比如 `Point` 结构体上,实现 `OutlinePrint` 时会发生什么:
```rust
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
```
咱们会得到一个声称要求 `Display` 当其未实现的报错:
```console
$ cargo run
Compiling supertrait v0.1.0 (/home/lenny.peng/rust-lang/supertrait)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:21:23
|
21 | 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`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `supertrait` due to previous error
```
为修复这个问题,咱们就要在 `Point` 上实现 `Display` 并满足 `OutlinePrint` 所需的约束,如下面这样:
```rust
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write! (f, "({}, {})", self.x, self.y)
}
}
```
随后在 `Point` 上实现 `OutlinePrint` 就将成功编译,而咱们就可以在 `Point` 实例上调用 `outline_print` 来将其实现在星号轮廓里了。
## 使用新型模式在外层类型上实现外层的特质
**Using the Newtype Pattern to Implement External Traits on External Types**
第 10 章中的 [“在类型上实现特质”](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质) 小节咱们曾提到指明只有当特质或类型二者之一属于代码本地的时咱们才被允许在类型上实现特质的孤儿规则the orphan rule。而使用涉及到在元组结构体中创建出一个新类型的 *新型模式newtype pattern*,那么绕过这种限制便是可行的了。(咱们曾在第 5 章的 [“使用不带命名字段的元组结构体来创建不同类型”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用不带命名字段的元组结构体来创建不同类型) 小节谈到过元组结构体这种元组结构体讲有一个字段且将是围绕咱们要实现某个特质的类型的一个瘦封装a thin wrapper。随后这个封装类型便是咱们代码箱的本地类型了而咱们就可以在这个封装上实现那个特质了。所谓 *新型newtype*,是源自 Haskell 编程语言的一个术语。使用这种模式没有运行时性能代码,同时那个封装类型在编译时会被略去。
作为一个示例,就说咱们打算在 `Vec<T>` 上实现 `Display`,而由于 `Display` 特质与 `Vec<T>` 类型,均被定义在咱们代码箱外部,因此孤儿规则会阻止咱们直接这样做。咱们可以构造一个保存着 `Vec<T>` 类型实例的 `Wrapper`;随后咱们就可以在 `Wrapper` 上实现 `Display`,并使用那个 `Vec<T>` 值,如下清单 19-23 中所示。
文件名:`src/main.rs`
```rust
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("你好"), String::from("世界")]);
println! ("w = {}", w);
}
```
*清单 19-23创建一个围绕 `Vec<String>``Wrapper` 类型来实现 `Display`*
由于 `Wrapper` 是个元组结构体,且 `Vec<T>` 是该元组中位于索引 `0` 处的项目,因此其中 `Display` 的实现,便使用了 `self.0` 来方法那个内部的 `Vec<T>`。随后咱们就可以在 `Wrapper` 上使用 `Display` 的功能了。
使用这种技巧的缺点,则是那个 `Wrapper` 是个新的类型,因此其没有他所保存值的那些方法。咱们讲必须直接在 `Wrapper` 上,实现 `Vec<T>` 的全部方法,即委托给 `self.0` 的那些方法,这就会允许咱们将 `Wrapper` 完全当作 `Vec<T>` 那样对待了。而若咱们想要这个新的类型,有着那个内部类型所有的全部方法,那么在 `Wrapper` 上实现 `Deref` 特质(曾在第 15 章的 [“运用 `Deref` 特质将灵巧指针像常规引用那样对待”](Ch15_Smart_Pointers.md#通过实现-deref-特质而像引用那样对待某个类型) 小节讨论过),来返回那个内部类型,将是一种办法。而若咱们不打算 `Wrapper` 类型有着内部类型的所有方法 -- 比如,为限制 `Wrapper` 的行为 -- 咱们就必须手动实现仅咱们想要的那些方法了。
即使不牵涉到特质,这种新型模式也是有用的。接下来就要转换一下视角,而看看与 Rust 的类型系统交互的一些高级方式。