高级特质
在第 10 章 “特质:定义共用行为” 小节中,咱们曾首先涉及到特质,但咱们不曾讨论更为高级的那些细节。现在咱们对 Rust 有了更多了解,咱们就可以深入本质,get into the nitty-gritty。
使用关联类型指定出特质定义中的一些占位性类型
Specifying placeholder types in trait definitions with associated types
关联类型 将类型占位符与特质加以结合,从而那些特质方法的定义,就可以在他们的签名中,使用这些占位符类型。特质的实现者,将为其特定实现,指明占位符类型所要使用的具体类型。如此一来,咱们便可以在特质被实现之前,无需准确获悉特质用到的类型下,定义出用到这些类型的特质。
在本章中,咱们已经介绍了绝大多数极少需要用到的高级特性。而关联类型则是位于这些高级特性中部的一种:其相较本书其余部分降到的那些特性,用得尤其少见,但相较这一章中讨论到的其他特性,则其要更常用一些。
带有关联类型特质的一个示例,便是标准库所提供的 Iterator
特质。其中的关联类型名为 Item
,且代表着实现了这个 Iterator
特质的类型所迭代的那些值的类型。Iterator
特质的定义如下清单 19-12 中所示。
#![allow(unused)] fn main() { 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
实现:
#![allow(unused)] fn main() { impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { // --跳过代码-- }
这种语法似乎可与泛型的那种语法相比。那么为何没有只使用泛型定义 Iterator
,如下清单 19-13 中所示的那样呢?
#![allow(unused)] fn main() { 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 了解 Java 中的重载与重写区别。
在咱们用到泛型参数时,咱们可以给泛型指定默认具体类型。在所指定的默认类型就有效时,这样做消除了实现者指定具体类型的需求。在声明泛型时使用 <PlaceholderType=ConcreteType>
语法,指定出默认类型。
这种技巧有用处情形的一个了不起示例,便是 运算符重载,operator overloading,咱们可以其在某些情形下,定制某个运算符(比如 +
)的行为。
Rust 不允许咱们创建自己的运算符,或重载任意运算符。但咱们可以通过实现与运算符相关的特质,而重载那些运算及于 std::ops
中所列出的相应特质。比如,在下面清单 19-14 中,咱们就将 +
运算符过载为把两个 Point
实例加在一起。咱们是通过在 Point
结构体上实现 Add
特质完成这一点的。
文件名: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
特质来为 Point
实例过载 +
运算符
这里的 add
方法,将两个 Point
实例的 x
值及两个实例的 y
值相加,而创建出一个新的 Point
。这里的 Add
特质有着一个确定自其中的 add
方法返回类型,名为的 Output
关联类型。
此代码中的默认泛型,是在 Add
特质里。以下便是其定义:
#![allow(unused)] fn main() { 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 中所示。
#![allow(unused)] fn main() { #[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
都完成不同的事情。
#![allow(unused)] fn main() { 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 中所示。
fn main() { let person = Human; person.fly(); }
清单 19-17:调用 Human
实例上的 fly
运行此代码将打印出 *愤怒地挥动双臂*
,显示 Rust 调用了直接在 Human
上实现的那个 fly
方法。
为了调用 Pilot
或 Wizard
特质上的 fly
方法,咱们需要使用更为显式的语法,来指明我们所指的是那个 fly
方法。下面清单 19-18 对此语法进行了演示。
文件名:src/main.rs
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()
等级,但若咱们无需消除歧义,这样写起来就些许有些长了。
运行此代码会打印以下输出:
$ 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
。
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
上的那个关联函数。此代码会打印下面的输出:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/disambiguation`
狗崽叫做 点点
此输出不是咱们想要的。咱们想要调用作为咱们曾在 Dog
上实现过的 Animal
特质一部分的那个 baby_name
函数,从而代码会打印出 小狗叫做 狗崽
。咱们曾在清单 19-18 中用到的指定特质名字的技巧,这里就不管用了;而若咱们将 main
修改为下面清单 19-20 中的代码,咱们就将收到一个编译报错。
fn main() { println! ("小狗叫做 {}", Animal::baby_name()); }
清单 19-20:尝试调用 Animal
特质中的那个 baby_name
函数,但 Rust 不清楚要使用那个实现
由于 Animal::baby_name
没有 self
参数,且这里可能有别的实现了 Animal
特质的类型,因此 Rust 就无法计算出咱们想要的那个 Animal::baby_name
实现。咱们将得到下面这个编译器错误:
$ 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
fn main() { println! ("小狗叫做 {}", <Dog as Animal>::baby_name()); }
清单 19-21:使用完全合格语法来指明,咱们是要调用实现在 Dog
上的 Animal
特质中的那个 baby_name
函数
通过讲出咱们希望将 Dog
类型,针对这个 baby_name
函数调用而作为 Animal
对待,从而表明咱们打算调用实现在 Dog
上的 Animal
特质中的 baby_name
方法,这样位处那尖括号中的类型注解,提供给 Rust。此代码现在将打印出咱们想要的输出:
$ 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`
小狗叫做 狗崽
一般来讲,完全合格语法是像下面这样定义的:
#![allow(unused)] fn main() { <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
时,其将打印以下输出:
**********
* *
* (1, 3) *
* *
**********
在 outline_print
方法的实现中,咱们打算使用 Display
特质的功能。因此,咱们就需要指明,这个 OutlinePrint
特质将只对那些同时实现了 Display
生效,且提供了 OutlinePrint
所需的功能。咱们可以通过指明 OutlinePrint: Display
,在该特质定义中实现那一点。这种技巧类似于给特质添加特质边界。下面清单 19-22 给出了这个 OutlinePrint
特质的一种实现。
#![allow(unused)] fn main() { 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
时会发生什么:
#![allow(unused)] fn main() { struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} }
咱们会得到一个声称要求 Display
当其未实现的报错:
$ 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
所需的约束,如下面这样:
#![allow(unused)] fn main() { 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 章中的 “在类型上实现特质” 小节,咱们曾提到,指明只有当特质或类型二者之一,属于代码本地的时,咱们才被允许在类型上实现特质的孤儿规则,the orphan rule。而使用涉及到在元组结构体中创建出一个新类型的 新型模式,newtype pattern,那么绕过这种限制便是可行的了。(咱们曾在第 5 章的 “使用不带命名字段的元组结构体来创建不同类型” 小节,谈到过元组结构体)这种元组结构体讲有一个字段,且将是围绕咱们要实现某个特质的类型的一个瘦封装,a thin wrapper。随后这个封装类型,便是咱们代码箱的本地类型了,而咱们就可以在这个封装上实现那个特质了。所谓 新型,newtype,是源自 Haskell 编程语言的一个术语。使用这种模式没有运行时性能代码,同时那个封装类型在编译时会被略去。
作为一个示例,就说咱们打算在 Vec<T>
上实现 Display
,而由于 Display
特质与 Vec<T>
类型,均被定义在咱们代码箱外部,因此孤儿规则会阻止咱们直接这样做。咱们可以构造一个保存着 Vec<T>
类型实例的 Wrapper
;随后咱们就可以在 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("你好"), 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
特质将灵巧指针像常规引用那样对待” 小节讨论过),来返回那个内部类型,将是一种办法。而若咱们不打算 Wrapper
类型有着内部类型的所有方法 -- 比如,为限制 Wrapper
的行为 -- 咱们就必须手动实现仅咱们想要的那些方法了。
即使不牵涉到特质,这种新型模式也是有用的。接下来就要转换一下视角,而看看与 Rust 的类型系统交互的一些高级方式。