|
|
|
@ -1,8 +1,7 @@
|
|
|
|
|
## Trait:定义共同行为
|
|
|
|
|
|
|
|
|
|
> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/main/src/ch10-02-traits.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit 92bfbfacf88ee9a814cea0a58e9c019c529ef4ae
|
|
|
|
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch10-02-traits.md -->
|
|
|
|
|
<!-- commit e5c8a0c99d5d3d830ad35e67f203a3378b4de16f -->
|
|
|
|
|
|
|
|
|
|
*trait* 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共同行为。可以使用 *trait bounds* 指定泛型是任何拥有特定行为的类型。
|
|
|
|
|
|
|
|
|
@ -12,9 +11,9 @@
|
|
|
|
|
|
|
|
|
|
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
|
|
|
|
|
|
|
|
|
|
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
|
|
|
|
|
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `SocialPost` 最多只能存放 280 个字符的内容,以及指示该帖子是新发布的、转发的还是对另一条帖子的回复的元数据。
|
|
|
|
|
|
|
|
|
|
我们想要创建一个名为 `aggregator` 的多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据摘要。为了实现功能,每个结构体都要能够获取摘要,这样的话就可以调用实例的 `summarize` 方法来请求摘要。示例 10-12 中展示了一个表现这个概念的公有 `Summary` trait 的定义:
|
|
|
|
|
我们想要创建一个名为 `aggregator` 的多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `SocialPost` 实例中的数据摘要。为了实现功能,每个结构体都要能够获取摘要,这样的话就可以调用实例的 `summarize` 方法来请求摘要。示例 10-12 中展示了一个表现这个概念的公有 `Summary` trait 的定义:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/lib.rs</span>
|
|
|
|
|
|
|
|
|
@ -24,7 +23,7 @@
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 10-12:`Summary` trait 定义,它包含由 `summarize` 方法提供的行为</span>
|
|
|
|
|
|
|
|
|
|
这里使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summary`。我们也声明 `trait` 为 `pub` 以便依赖这个 crate 的 crate 也可以使用这个 trait,正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。
|
|
|
|
|
这里使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summary`。我们也声明 `trait` 为 `pub` 以便依赖这个 crate 的其它 crate 也可以使用这个 trait,正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。
|
|
|
|
|
|
|
|
|
|
在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summary` trait 的类型都拥有与这个签名的定义完全一致的 `summarize` 方法。
|
|
|
|
|
|
|
|
|
@ -32,7 +31,7 @@ trait 体中可以有多个方法:一行一个方法签名且都以分号结
|
|
|
|
|
|
|
|
|
|
### 为类型实现 trait
|
|
|
|
|
|
|
|
|
|
现在我们定义了 `Summary` trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `Tweet` 结构体,我们选择将 `summarize` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。
|
|
|
|
|
现在我们定义了 `Summary` trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `SocialPost` 结构体,我们选择将 `summarize` 定义为用户名后跟帖子全文作为返回值,并假设帖子内容已经被限制为 280 字符以内。
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/lib.rs</span>
|
|
|
|
|
|
|
|
|
@ -40,21 +39,21 @@ trait 体中可以有多个方法:一行一个方法签名且都以分号结
|
|
|
|
|
{{#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>
|
|
|
|
|
<span class="caption">示例 10-13:在 `NewsArticle` 和 `SocialPost` 类型上实现 `Summary` trait</span>
|
|
|
|
|
|
|
|
|
|
在类型上实现 trait 类似于实现常规方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
|
|
|
|
|
|
|
|
|
|
现在库在 `NewsArticle` 和 `Tweet` 上实现了`Summary` trait,crate 的用户可以像调用常规方法一样调用 `NewsArticle` 和 `Tweet` 实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 `aggregator` 库 crate 的例子:
|
|
|
|
|
现在库在 `NewsArticle` 和 `SocialPost` 上实现了`Summary` trait,crate 的用户可以像调用常规方法一样调用 `NewsArticle` 和 `SocialPost` 实例的 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`。
|
|
|
|
|
这会打印出 `1 new post: horse_ebooks: of course, as you probably already know, people`。
|
|
|
|
|
|
|
|
|
|
其他依赖 `aggregator` crate 的 crate 也可以将 `Summary` 引入作用域以便为其自己的类型实现该 trait。需要注意的限制是,只有在 trait 或类型至少有一个属于当前 crate 时,我们才能对类型实现该 trait。例如,可以为 `aggregator` crate 的自定义类型 `Tweet` 实现如标准库中的 `Display` trait,这是因为 `Tweet` 类型位于 `aggregator` crate 本地的作用域中。类似地,也可以在 `aggregator` crate 中为 `Vec<T>` 实现 `Summary`,这是因为 `Summary` trait 位于 `aggregator` crate 本地作用域中。
|
|
|
|
|
其他依赖 `aggregator` crate 的 crate 也可以将 `Summary` 引入作用域以便为其自己的类型实现该 trait。需要注意的限制是,只有在 trait 或类型至少有一个属于当前 crate 时,我们才能对类型实现该 trait。例如,可以为 `aggregator` crate 的自定义类型 `SocialPost` 实现如标准库中的 `Display` trait,这是因为 `SocialPost` 类型位于 `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。例如,不能在 `aggregator` crate 中为 `Vec<T>` 实现 `Display` trait。这是因为 `Display` 和 `Vec<T>` 都定义于标准库中,它们并不位于 `aggregator` crate 本地作用域中。这个限制是被称为**相干性**(*coherence*)的程序属性的一部分,或者更具体的说是 **孤儿规则**(*orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你的代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
|
|
|
|
|
|
|
|
|
|
### 默认实现
|
|
|
|
|
|
|
|
|
@ -80,7 +79,7 @@ trait 体中可以有多个方法:一行一个方法签名且都以分号结
|
|
|
|
|
|
|
|
|
|
这段代码会打印 `New article available! (Read more...)`。
|
|
|
|
|
|
|
|
|
|
为 `summarize` 创建默认实现并不要求对示例 10-13 中 `Tweet` 上的 `Summary` 实现做任何改变。其原因是重载一个默认实现的语法与实现没有默认实现的 trait 方法的语法一样。
|
|
|
|
|
为 `summarize` 创建默认实现并不要求对示例 10-13 中 `SocialPost` 上的 `Summary` 实现做任何改变。其原因是重载一个默认实现的语法与实现没有默认实现的 trait 方法的语法一样。
|
|
|
|
|
|
|
|
|
|
默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以提供很多有用的功能而只需要实现指定一小部分内容。例如,我们可以定义 `Summary` trait,使其具有一个需要实现的 `summarize_author` 方法,然后定义一个 `summarize` 方法,此方法的默认实现调用 `summarize_author` 方法:
|
|
|
|
|
|
|
|
|
@ -88,31 +87,31 @@ trait 体中可以有多个方法:一行一个方法签名且都以分号结
|
|
|
|
|
{{#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` 即可:
|
|
|
|
|
为了使用这个版本的 `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` 方法的功能,且无需编写更多的代码。
|
|
|
|
|
一旦定义了 `summarize_author`,我们就可以对 `SocialPost` 结构体的实例调用 `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...)`。
|
|
|
|
|
这会打印出 `1 new post: (Read more from @horse_ebooks...)`。
|
|
|
|
|
|
|
|
|
|
注意无法从相同方法的重载实现中调用默认方法。
|
|
|
|
|
|
|
|
|
|
### trait 作为参数
|
|
|
|
|
|
|
|
|
|
知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。示例 10-13 中为 `NewsArticle` 和 `Tweet` 类型实现了 `Summary` trait,用其来定义了一个函数 `notify` 来调用其参数 `item` 上的 `summarize` 方法,该参数是实现了 `Summary` trait 的某种类型。为此可以使用 `impl Trait` 语法,像这样:
|
|
|
|
|
知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。示例 10-13 中为 `NewsArticle` 和 `SocialPost` 类型实现了 `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`。
|
|
|
|
|
对于 `item` 参数,我们指定了 `impl` 关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在 `notify` 函数体中,可以调用任何来自 `Summary` trait 的方法,比如 `summarize`。我们可以传递任何 `NewsArticle` 或 `SocialPost` 的实例来调用 `notify`。任何用其它如 `String` 或 `i32` 的类型调用该函数的代码都不能编译,因为它们没有实现 `Summary`。
|
|
|
|
|
|
|
|
|
|
#### Trait Bound 语法
|
|
|
|
|
|
|
|
|
@ -124,7 +123,7 @@ pub fn notify<T: Summary>(item: &T) {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这与之前的例子相同,不过稍微冗长了一些。trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。
|
|
|
|
|
这种更冗长的写法与上一节的示例等价,但更为冗长。trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。
|
|
|
|
|
|
|
|
|
|
`impl Trait` 很方便,适用于短小的例子。更长的 trait bound 则适用于更复杂的场景。例如,可以获取两个实现了 `Summary` 的参数。使用 `impl Trait` 的语法看起来像这样:
|
|
|
|
|
|
|
|
|
@ -132,7 +131,7 @@ pub fn notify<T: Summary>(item: &T) {
|
|
|
|
|
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这适用于 `item1` 和 `item2` 允许是不同类型的情况(只要它们都实现了 `Summary`)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:
|
|
|
|
|
这适用于 `item1` 和 `item2` 允许是不同类型的情况(只要它们都实现了 `Summary`)。不过如果你希望强制它们都是相同类型呢?但如果我们希望强制两个参数必须具有相同类型,则必须使用 trait bound,如下所示:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
pub fn notify<T: Summary>(item1: &T, item2: &T) {
|
|
|
|
@ -142,7 +141,7 @@ pub fn notify<T: Summary>(item1: &T, item2: &T) {
|
|
|
|
|
|
|
|
|
|
#### 通过 `+` 指定多个 trait bound
|
|
|
|
|
|
|
|
|
|
如果 `notify` 需要显示 `item` 的格式化形式,同时也要使用 `summarize` 方法,那么 `item` 就需要同时实现两个不同的 trait:`Display` 和 `Summary`。这可以通过 `+` 语法实现:
|
|
|
|
|
我们也可以指定多个 trait bound。假设我们希望 `notify` 在 `item` 上既能使用格式化显示,又能使用 `summarize` 方法:在 `notify` 的定义中,指定 `item` 必须同时实现 `Display` 和 `Summary` 两个 trait。这可以通过 `+` 语法实现:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
pub fn notify(item: &(impl Summary + Display)) {
|
|
|
|
@ -180,17 +179,17 @@ fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
|
|
|
|
|
{{#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`,不过调用方并不知情。
|
|
|
|
|
通过使用 `impl Summary` 作为返回值类型,我们指定了 `returns_summarizable` 函数返回某个实现了 `Summary` trait 的类型,但是不确定其具体的类型。在这个例子中 `returns_summarizable` 返回了一个 `SocialPost`,不过调用方并不知情。
|
|
|
|
|
|
|
|
|
|
返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,第十三章会介绍它们。闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。`impl Trait` 允许你简单的指定函数返回一个 `Iterator` 而无需写出实际的冗长的类型。
|
|
|
|
|
|
|
|
|
|
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 `impl Summary`,但是返回了 `NewsArticle` 或 `Tweet` 就行不通:
|
|
|
|
|
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 `impl Summary`,但是返回了 `NewsArticle` 或 `SocialPost` 就行不通:
|
|
|
|
|
|
|
|
|
|
```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] 部分会介绍如何编写这样一个函数。
|
|
|
|
|
这里尝试返回 `NewsArticle` 或 `SocialPost` 是不被允许的,原因在于编译器中 `impl Trait` 语法的实现限制。。第十八章的 [“顾及不同类型值的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分会介绍如何编写这样一个函数。
|
|
|
|
|
|
|
|
|
|
### 使用 trait bound 有条件地实现方法
|
|
|
|
|
|
|
|
|
@ -216,9 +215,9 @@ impl<T: Display> ToString for T {
|
|
|
|
|
let s = 3.to_string();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
blanket implementation 会出现在 trait 文档的 “Implementers” 部分。
|
|
|
|
|
Blanket implementation 会出现在 trait 文档的 “Implementers” 部分。
|
|
|
|
|
|
|
|
|
|
trait 和 trait bound 让我们能够使用泛型类型参数来减少重复,而且能够向编译器明确指定泛型类型需要拥有哪些行为。然后编译器可以利用 trait bound 信息检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们调用了一个未定义的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复问题。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了。这样既提升了性能又不必放弃泛型的灵活性。
|
|
|
|
|
Trait 和 trait bound 让我们能够使用泛型类型参数来减少重复,而且能够向编译器明确指定泛型类型需要拥有哪些行为。然后编译器可以利用 trait bound 信息检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们调用了一个未定义的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复问题。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了。这样既提升了性能又不必放弃泛型的灵活性。
|
|
|
|
|
|
|
|
|
|
[using-trait-objects-that-allow-for-values-of-different-types]:
|
|
|
|
|
ch18-02-trait-objects.html#顾及不同类型值的-trait-对象
|
|
|
|
|