You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trpl-zh-cn/src/ch10-02-traits.md

226 lines
16 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## Trait定义共同行为
> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/main/src/ch10-02-traits.md)
> <br>
> commit 92bfbfacf88ee9a814cea0a58e9c019c529ef4ae
*trait* 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 *trait bounds* 指定泛型是任何拥有特定行为的类型。
> 注意:*trait* 类似于其他语言中的常被称为 **接口***interfaces*)的功能,虽然有一些不同。
### 定义 trait
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个名为 `aggregator` 的多媒体聚合库用来显示可能储存在 `NewsArticle``Tweet` 实例中的数据摘要。为了实现功能,每个结构体都要能够获取摘要,这样的话就可以调用实例的 `summarize` 方法来请求摘要。示例 10-12 中展示了一个表现这个概念的公有 `Summary` trait 的定义:
<span class="filename">文件名src/lib.rs</span>
```rust,noplayground
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-12/src/lib.rs}}
```
<span class="caption">示例 10-12`Summary` trait 定义,它包含由 `summarize` 方法提供的行为</span>
这里使用 `trait` 关键字来声明一个 trait后面是 trait 的名字,在这个例子中是 `Summary`。我们也声明 `trait` 为 `pub` 以便依赖这个 crate 的 crate 也可以使用这个 trait正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。
在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summary` trait 的类型都拥有与这个签名的定义完全一致的 `summarize` 方法。
trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。
### 为类型实现 trait
现在我们定义了 `Summary` trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `Tweet` 结构体,我们选择将 `summarize` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。
<span class="filename">文件名src/lib.rs</span>
```rust,noplayground
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-13/src/lib.rs:here}}
```
<span class="caption">示例 10-13`NewsArticle``Tweet` 类型上实现 `Summary` trait</span>
在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
现在库在 `NewsArticle``Tweet` 上实现了`Summary` traitcrate 的用户可以像调用常规方法一样调用 `NewsArticle``Tweet` 实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 `aggregator` 库 crate 的例子:
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-01-calling-trait-method/src/main.rs}}
```
这会打印出 `1 new tweet: horse_ebooks: of course, as you probably already know, people`。
其他依赖 `aggregator` crate 的 crate 也可以将 `Summary` 引入作用域以便为其自己的类型实现该 trait。实现 trait 时需要注意的一个限制是,只有当至少一个 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如可以为 `aggregator` crate 的自定义类型 `Tweet` 实现如标准库中的 `Display` trait这是因为 `Tweet` 类型位于 `aggregator` crate 本地的作用域中。类似地,也可以在 `aggregator` crate 中为 `Vec<T>` 实现 `Summary`,这是因为 `Summary` trait 位于 `aggregator` crate 本地作用域中。
但是不能为外部类型实现外部 trait。例如不能在 `aggregator` crate 中为 `Vec<T>` 实现 `Display` trait。这是因为 `Display` 和 `Vec<T>` 都定义于标准库中,它们并不位于 `aggregator` crate 本地作用域中。这个限制是被称为 **相干性***coherence*)的程序属性的一部分,或者更具体的说是 **孤儿规则***orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait而 Rust 将无从得知应该使用哪一个实现。
### 默认实现
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
示例 10-14 中我们为 `Summary` trait 的 `summarize` 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名:
<span class="filename">文件名src/lib.rs</span>
```rust,noplayground
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-14/src/lib.rs:here}}
```
<span class="caption">示例 10-14`Summary` trait 的定义,带有一个 `summarize` 方法的默认实现</span>
如果想要对 `NewsArticle` 实例使用这个默认实现,可以通过 `impl Summary for NewsArticle {}` 指定一个空的 `impl` 块。
虽然我们不再直接为 `NewsArticle` 定义 `summarize` 方法了,但是我们提供了一个默认实现并且指定 `NewsArticle` 实现 `Summary` trait。因此我们仍然可以对 `NewsArticle` 实例调用 `summarize` 方法,如下所示:
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-02-calling-default-impl/src/main.rs:here}}
```
这段代码会打印 `New article available! (Read more...)`。
为 `summarize` 创建默认实现并不要求对示例 10-13 中 `Tweet` 上的 `Summary` 实现做任何改变。其原因是重载一个默认实现的语法与实现没有默认实现的 trait 方法的语法一样。
默认实现允许调用相同 trait 中的其他方法哪怕这些方法没有默认实现。如此trait 可以提供很多有用的功能而只需要实现指定一小部分内容。例如,我们可以定义 `Summary` trait使其具有一个需要实现的 `summarize_author` 方法,然后定义一个 `summarize` 方法,此方法的默认实现调用 `summarize_author` 方法:
```rust,noplayground
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:here}}
```
为了使用这个版本的 `Summary`,只需在实现 trait 时定义 `summarize_author` 即可:
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:impl}}
```
一旦定义了 `summarize_author`,我们就可以对 `Tweet` 结构体的实例调用 `summarize` 了,而 `summarize` 的默认实现会调用我们提供的 `summarize_author` 定义。因为实现了 `summarize_author``Summary` trait 就提供了 `summarize` 方法的功能,且无需编写更多的代码。
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/main.rs:here}}
```
这会打印出 `1 new tweet: (Read more from @horse_ebooks...)`
注意无法从相同方法的重载实现中调用默认方法。
### trait 作为参数
知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。示例 10-13 中为 `NewsArticle``Tweet` 类型实现了 `Summary` trait用其来定义了一个函数 `notify` 来调用其参数 `item` 上的 `summarize` 方法,该参数是实现了 `Summary` trait 的某种类型。为此可以使用 `impl Trait` 语法,像这样:
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-04-traits-as-parameters/src/lib.rs:here}}
```
对于 `item` 参数,我们指定了 `impl` 关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在 `notify` 函数体中,可以调用任何来自 `Summary` trait 的方法,比如 `summarize`。我们可以传递任何 `NewsArticle` 或 `Tweet` 的实例来调用 `notify`。任何用其它如 `String` 或 `i32` 的类型调用该函数的代码都不能编译,因为它们没有实现 `Summary`。
#### Trait Bound 语法
`impl Trait` 语法适用于直观的例子,它实际上是一种较长形式语法的语法糖。我们称为 *trait bound*,它看起来像:
```rust,ignore
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
```
这与之前的例子相同不过稍微冗长了一些。trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。
`impl Trait` 很方便,适用于短小的例子。更长的 trait bound 则适用于更复杂的场景。例如,可以获取两个实现了 `Summary` 的参数。使用 `impl Trait` 的语法看起来像这样:
```rust,ignore
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
```
这适用于 `item1` 和 `item2` 允许是不同类型的情况(只要它们都实现了 `Summary`)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:
```rust,ignore
pub fn notify<T: Summary>(item1: &T, item2: &T) {
```
泛型 `T` 被指定为 `item1``item2` 的参数限制,如此传递给参数 `item1``item2` 值的具体类型必须一致。
#### 通过 `+` 指定多个 trait bound
如果 `notify` 需要显示 `item` 的格式化形式,同时也要使用 `summarize` 方法,那么 `item` 就需要同时实现两个不同的 trait`Display` 和 `Summary`。这可以通过 `+` 语法实现:
```rust,ignore
pub fn notify(item: &(impl Summary + Display)) {
```
`+` 语法也适用于泛型的 trait bound
```rust,ignore
pub fn notify<T: Summary + Display>(item: &T) {
```
通过指定这两个 trait bound`notify` 的函数体可以调用 `summarize` 并使用 `{}` 来格式化 `item`
#### 通过 `where` 简化 trait bound
然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息这使得函数签名难以阅读。为此Rust 有另一个在函数签名之后的 `where` 从句中指定 trait bound 的语法。所以除了这么写:
```rust,ignore
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
```
还可以像这样使用 `where` 从句:
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-where-clause/src/lib.rs:here}}
```
这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。
### 返回实现了 trait 的类型
也可以在返回值中使用 `impl Trait` 语法,来返回实现了某个 trait 的类型:
```rust,ignore
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-05-returning-impl-trait/src/lib.rs:here}}
```
通过使用 `impl Summary` 作为返回值类型,我们指定了 `returns_summarizable` 函数返回某个实现了 `Summary` trait 的类型,但是不确定其具体的类型。在这个例子中 `returns_summarizable` 返回了一个 `Tweet`,不过调用方并不知情。
返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,第十三章会介绍它们。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。`impl Trait` 允许你简单的指定函数返回一个 `Iterator` 而无需写出实际的冗长的类型。
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 `impl Summary`,但是返回了 `NewsArticle` 或 `Tweet` 就行不通:
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-06-impl-trait-returns-one-type/src/lib.rs:here}}
```
这里尝试返回 `NewsArticle``Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。第十七章的 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分会介绍如何编写这样一个函数。
### 使用 trait bound 有条件地实现方法
通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,示例 10-15 中的类型 `Pair<T>` 总是实现了 `new` 方法并返回一个 `Pair<T>` 的实例(回忆一下第五章的 ["定义方法"][methods] 部分,`Self` 是一个 `impl` 块类型的类型别名type alias在这里是 `Pair<T>`)。不过在下一个 `impl` 块中,只有那些为 `T` 类型实现了 `PartialOrd` trait (来允许比较) **和** `Display` trait (来启用打印)的 `Pair<T>` 才会实现 `cmp_display` 方法:
```rust,noplayground
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-15/src/lib.rs}}
```
<span class="caption">示例 10-15根据 trait bound 在泛型上有条件的实现方法</span>
也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 *blanket implementations*,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 `Display` trait 的类型实现了 `ToString` trait。这个 `impl` 块看起来像这样:
```rust,ignore
impl<T: Display> ToString for T {
// --snip--
}
```
因为标准库有了这些 blanket implementation我们可以对任何实现了 `Display` trait 的类型调用由 `ToString` 定义的 `to_string` 方法。例如,可以将整型转换为对应的 `String` 值,因为整型实现了 `Display`
```rust
let s = 3.to_string();
```
blanket implementation 会出现在 trait 文档的 “Implementers” 部分。
trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中如果我们尝试调用一个类型并没有实现的方法会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
[using-trait-objects-that-allow-for-values-of-different-types]:
ch17-02-trait-objects.html#为使用不同类型的值而设计的-trait-对象
[methods]: ch05-03-method-syntax.html#定义方法