# 高级特质 在第 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; } ``` *清单 19-12:有着关联类型 `Item` 的 `Iterator` 特质的定义* 其中的类型 `Item` 便是个占位符,而那个 `next` 方法的定义,则显示其将返回类型类型为 `Option` 的值。`Iterator` 的实现者,将指明 `Item` 的具体类型,同时 `next` 方法将返回包含那个具体类型值的一个 `Option`。 从泛型允许咱们在不指明函数可处理何种类型下,而定义出某个函数上看,关联类型可能看起来是个与泛型类似类似的概念。为检视这两个概念的不同,咱们将看看在指定了 `Item` 类型为 `u32` 的一个名为 `Counter` 的类型上的 `Iterator` 实现: ```rust impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option { // --跳过代码-- ``` 这种语法似乎可与泛型的那种语法相比。那么为何没有只使用泛型定义 `Iterator`,如下清单 19-13 中所示的那样呢? ```rust pub trait Iterator { fn next(&mut self) -> Option; } ``` *清单 19-13:使用泛型的一种 `Iterator` 特质的定义假设* 不同之处在于,如同清单 19-13 中使用泛型时,咱们必须注解每个实现中的那些类型;由于咱们还可以实现 `Iterfator 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 中的重载与重写区别。 在咱们用到泛型参数时,咱们可以给泛型指定默认具体类型。在所指定的默认类型就有效时,这样做消除了实现者指定具体类型的需求。在声明泛型时使用 `` 语法,指定出默认类型。 这种技巧有用处情形的一个了不起示例,便是 *运算符重载,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 { 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 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` 来设置那个 `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! ("小狗叫做 {}", ::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! ("小狗叫做 {}", ::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 ::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` 上实现 `Display`,而由于 `Display` 特质与 `Vec` 类型,均被定义在咱们代码箱外部,因此孤儿规则会阻止咱们直接这样做。咱们可以构造一个保存着 `Vec` 类型实例的 `Wrapper`;随后咱们就可以在 `Wrapper` 上实现 `Display`,并使用那个 `Vec` 值,如下清单 19-23 中所示。 文件名:`src/main.rs` ```rust use std::fmt; struct Wrapper(Vec); 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` 的 `Wrapper` 类型来实现 `Display`* 由于 `Wrapper` 是个元组结构体,且 `Vec` 是该元组中位于索引 `0` 处的项目,因此其中 `Display` 的实现,便使用了 `self.0` 来方法那个内部的 `Vec`。随后咱们就可以在 `Wrapper` 上使用 `Display` 的功能了。 使用这种技巧的缺点,则是那个 `Wrapper` 是个新的类型,因此其没有他所保存值的那些方法。咱们讲必须直接在 `Wrapper` 上,实现 `Vec` 的全部方法,即委托给 `self.0` 的那些方法,这就会允许咱们将 `Wrapper` 完全当作 `Vec` 那样对待了。而若咱们想要这个新的类型,有着那个内部类型所有的全部方法,那么在 `Wrapper` 上实现 `Deref` 特质(曾在第 15 章的 [“运用 `Deref` 特质将灵巧指针像常规引用那样对待”](Ch15_Smart_Pointers.md#通过实现-deref-特质而像引用那样对待某个类型) 小节讨论过),来返回那个内部类型,将是一种办法。而若咱们不打算 `Wrapper` 类型有着内部类型的所有方法 -- 比如,为限制 `Wrapper` 的行为 -- 咱们就必须手动实现仅咱们想要的那些方法了。 即使不牵涉到特质,这种新型模式也是有用的。接下来就要转换一下视角,而看看与 Rust 的类型系统交互的一些高级方式。