mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
57a0d64933
@ -102,4 +102,19 @@
|
||||
- [模式用来匹配值的结构](ch18-00-patterns.md)
|
||||
- [所有可能会用到模式的位置](ch18-01-all-the-places-for-patterns.md)
|
||||
- [refutable:何时模式可能会匹配失败](ch18-02-refutability.md)
|
||||
- [模式的全部语法](ch18-03-pattern-syntax.md)
|
||||
- [模式的全部语法](ch18-03-pattern-syntax.md)
|
||||
|
||||
- [高级特征](ch19-00-advanced-features.md)
|
||||
- [不安全的 Rust](ch19-01-unsafe-rust.md)
|
||||
- [高级生命周期](ch19-02-advanced-lifetimes.md)
|
||||
- [高级 trait](ch19-03-advanced-traits.md)
|
||||
- [高级类型](ch19-04-advanced-types.md)
|
||||
- [高级函数与闭包](ch19-05-advanced-functions-and-closures.md)
|
||||
|
||||
- [Final Project: Building a Multithreaded Web Server](ch20-00-final-project-a-web-server.md)
|
||||
- [A Single Threaded Web Server](ch20-01-single-threaded.md)
|
||||
- [How Slow Requests Affect Throughput](ch20-02-slow-requests.md)
|
||||
- [Designing the Thread Pool Interface](ch20-03-designing-the-interface.md)
|
||||
- [Creating the Thread Pool and Storing Threads](ch20-04-storing-threads.md)
|
||||
- [Sending Requests to Threads Via Channels](ch20-05-sending-requests-via-channels.md)
|
||||
- [Graceful Shutdown and Cleanup](ch20-06-graceful-shutdown-and-cleanup.md)
|
@ -56,7 +56,7 @@ fn calculate_length(s: &String) -> usize { // s is a reference to a String
|
||||
|
||||
变量`s`有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。
|
||||
|
||||
我们将获取引用作为函数参数称为**借用**(*borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。
|
||||
我们将获取引用作为函数参数称为**借用**(*borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
|
||||
|
||||
如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!
|
||||
|
||||
|
@ -105,7 +105,7 @@ communicator
|
||||
└── client
|
||||
```
|
||||
|
||||
可以看到列表 7-2 中,`client`是`network`的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。
|
||||
可以看到列表 7-2 中,`client`是`network`的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。
|
||||
|
||||
### 将模块移动到其他文件
|
||||
|
||||
@ -348,4 +348,4 @@ communicator
|
||||
|
||||
模块自身则应该使用`mod`关键字定义于父模块的文件中。
|
||||
|
||||
接下来,我们讨论一下`pub`关键字,并除掉那些警告!
|
||||
接下来,我们讨论一下`pub`关键字,并除掉那些警告!
|
||||
|
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit e6d6caab41471f7115a621029bd428a812c5260e
|
||||
|
||||
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。
|
||||
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这时我们可能想要创建这个文件而不是终止进程。
|
||||
|
||||
回忆一下第二章“使用`Result`类型来处理潜在的错误”部分中的那个`Result`枚举,它定义有如下两个成员,`Ok`和`Err`:
|
||||
|
||||
@ -298,4 +298,4 @@ error[E0308]: mismatched types
|
||||
|
||||
错误指出存在不匹配的类型:`main`函数返回一个`()`类型,而`?`返回一个`Result`。编写不返回`Result`的函数时,如果调用其他返回`Result`的函数,需要使用`match`或者`Result`的方法之一来处理它,而不能用`?`将潜在的错误传播给调用者。
|
||||
|
||||
现在我们讨论过了调用`panic!`或返回`Result`的细节,是时候返回他们各自适合哪些场景的话题了。
|
||||
现在我们讨论过了调用`panic!`或返回`Result`的细节,是时候返回他们各自适合哪些场景的话题了。
|
||||
|
@ -13,7 +13,7 @@
|
||||
3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
|
||||
4. 重复上述步骤!
|
||||
|
||||
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测测试有助于在开发过程中保持高测试覆盖率。
|
||||
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
|
||||
|
||||
我们将测试驱动实现`greprs`实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做`search`的函数中增加这些功能。
|
||||
|
||||
@ -296,4 +296,4 @@ $ cargo run monomorphization poem.txt
|
||||
|
||||
非常好!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。
|
||||
|
||||
现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。
|
||||
现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。
|
||||
|
@ -4,25 +4,26 @@
|
||||
> <br>
|
||||
> commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9
|
||||
|
||||
在第八章,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了存放包含整型、浮点型和文本型成员的枚举类型`SpreadsheetCell`,这样就可以在每一个单元格储存不同类型的数据,并使得 vector 仍然代表一行单元格。当编译时就知道类型集合全部元素的情况下,这种方案是可行的。
|
||||
在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了一个拥有分别存放整型、浮点型和文本型成员的枚举类型 `SpreadsheetCell`,使用这个枚举的 vector 可以在每一个单元格(cell)中储存不同类型的数据,并使得 vector 整体仍然代表一行(row)单元格。这当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是可行的。
|
||||
|
||||
<!-- The code example I want to reference did not have a listing number; it's
|
||||
the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
|
||||
get Chapter 8 for editing. /Carol -->
|
||||
|
||||
有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用 `draw` 方法来绘制在屏幕上。我们将要创建一个叫做 `rust_gui` 的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如 `Button` 或 `TextField`。使用 `rust_gui` 的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个 `Image`,而另一个可能会增加一个 `SelectBox`。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
|
||||
有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个项目列表的概念,它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上。我们将要创建一个叫做 `rust_gui` 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button` 或 `TextField`。使用 `rust_gui` 的程序员会想要创建更多可以绘制在屏幕上的类型:其中一些可能会增加一个 `Image`,而另一些可能会增加一个 `SelectBox`。本章节并不准备实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。
|
||||
|
||||
当写 `rust_gui` 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 `enum` 来包含所有的类型。然而 `rust_gui` 需要跟踪所有这些不同类型的值,需要有在每个值上调用 `draw` 方法能力。我们的 GUI 库不需要确切地知道调用 `draw` 方法会发生什么,只需要有可用的方法供我们调用。
|
||||
编写 `rust_gui` 库时,我们并不知道其他程序员想要创建的全部类型,所以无法定义一个 `enum` 来包含所有这些类型。我们所要做的是使 `rust_gui` 能够记录一系列不同类型的值,并能够对其中每一个值调用 `draw` 方法。 GUI 库不需要知道当调用 `draw` 方法时具体会发生什么,只需提供这些值可供调用的方法即可。
|
||||
|
||||
在可以继承的语言里,我们会定义一个名为 `Component` 的类,该类上有一个`draw`方法。其他的类比如`Button`、`Image`和`SelectBox`会从`Component`继承并拥有`draw`方法。它们各自覆写`draw`方法以自定义行为,但是框架会把所有的类型当作是`Component`的实例,并在其上调用`draw`。
|
||||
在拥有继承的语言中,我们可能定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。
|
||||
|
||||
### 定义一个带有自定义行为的Trait
|
||||
### 定义通用行为的 trait
|
||||
|
||||
不过,在Rust语言中,我们可以定义一个 `Draw` trait,包含名为 `draw` 的方法。我们定义一个由*trait对象*组成的vector,绑定了某种指针的trait,比如`&`引用或者一个`Box<T>`智能指针。
|
||||
不过,在 Rust 中,我们可以定义一个 `Draw` trait,包含名为 `draw` 的方法。接着可以定义一个存放**trait 对象**(*trait
|
||||
object*)的 vector,trait 对象是一个位于某些指针,比如 `&` 引用或 `Box<T>` 智能指针,之后的 trait。第十九章会讲到为何 trait 对象必须位于指针之后的原因。
|
||||
|
||||
之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和`impl`块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在 trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。
|
||||
之前提到过,我们并不将结构体与枚举称之为“对象”,以便与其他语言中的对象相区别。结构体与枚举和 `impl` 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将由指向具体对象的指针构成的数据和定义于 trait 中方法的行为结合在一起,从这种意义上说它**则**更类似其他语言中的对象。不过 trait 对象与其他语言中的对象是不同的,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:他们(trait 对象)的作用是允许对通用行为的抽象。
|
||||
|
||||
trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。列表 17-03 展示了如何定义一个名为`Draw`的带有`draw`方法的 trait。
|
||||
trait 对象定义了在给定情况下所需的行为。接着就可以在要使用具体类型或泛型的地方使用 trait 来作为 trait 对象。Rust 的类型系统会确保任何我们替换为 trait 对象的值都会实现了 trait 的方法。这样就无需在编译时就知道所有可能的类型,就能够用同样的方法处理所有的实例。列表 17-3 展示了如何定义一个带有 `draw` 方法的 trait `Draw`:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -36,7 +37,9 @@ pub trait Draw {
|
||||
|
||||
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
|
||||
|
||||
因为我们已经在第十章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:列表 17-4 有一个名为 `Screen` 的结构体,里面有一个名为 `components` 的 vector,`components` 的类型是 `Box<Draw>`。`Box<Draw>` 是一个 trait 对象:它是 `Box` 内部任意一个实现了 `Draw` trait 的类型的替身。
|
||||
|
||||
|
||||
因为第十章已经讨论过如何定义 trait,这看起来应该比较眼熟。接下来就是新内容了:列表 17-4 有一个名为 `Screen` 的结构体定义,它存放了一个叫做 `components` 的 `Box<Draw>` 类型的 vector 。`Box<Draw>` 是一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -52,7 +55,7 @@ pub struct Screen {
|
||||
|
||||
<span class="caption">列表 17-4: 一个 `Screen` 结构体的定义,它带有一个字段`components`,其包含实现了 `Draw` trait 的 trait 对象的 vector</span>
|
||||
|
||||
在 `Screen` 结构体上,我们将要定义一个 `run` 方法,该方法会在它的 `components` 上的每一个元素调用 `draw` 方法,如列表 17-5 所示:
|
||||
在 `Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个元素调用 `draw` 方法,如列表 17-5 所示:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -74,10 +77,9 @@ impl Screen {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 17-5:在 `Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法
|
||||
</span>
|
||||
<span class="caption">列表 17-5:在 `Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法</span>
|
||||
|
||||
这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 `Screen` 结构体使用泛型和一个 trait 约束,如列表 17-6 所示:
|
||||
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体的类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以像列表 17-6 那样定义使用泛型和 trait bound 的结构体 `Screen`:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -100,16 +102,15 @@ impl<T> Screen<T>
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 17-6: 一种 `Screen` 结构体的替代实现,它的 `run` 方法使用通用类型和 trait 绑定
|
||||
</span>
|
||||
<span class="caption">列表 17-6: 一种 `Screen` 结构体的替代实现,它的 `run` 方法使用泛型和 trait bound</span>
|
||||
|
||||
这个例子中,`Screen` 实例所有组件类型必需全是 `Button`,或者全是 `TextField`。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。
|
||||
这只允许我们拥有一个包含全是 `Button` 类型或者全是 `TextField` 类型的 component 列表的 `Screen` 实例。如果只拥有相同类型的集合,那么使用泛型和 trait bound 是更好的,因为在编译时使用具体类型其定义是单态(monomorphized)的。
|
||||
|
||||
而 `Screen` 结构体内部的 `Vec<Box<Draw>>` trait 对象列表,则可以同时包含 `Box<Button>` 和 `Box<TextField>`。我们看它是怎么工作的,然后讨论运行时性能。
|
||||
相反对于存放了 `Vec<Box<Draw>>` trait 对象的 component 列表的 `Screen` 定义,一个 `Screen` 实例可以存放一个既可以包含 `Box<Button>`,也可以包含 `Box<TextField>` 的 `Vec`。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
|
||||
|
||||
### 来自我们或者库使用者的实现
|
||||
### 来自我们或者库使用者的 trait 实现
|
||||
|
||||
现在,我们增加一些实现了 `Draw` trait 的类型,再次提供 `Button`。实现一个 GUI 库实际上超出了本书的范围,因此 `draw` 方法留空。为了想象实现可能的样子,`Button` 结构体有 `width`、`height` 和 `label`字段,如列表 17-7 所示:
|
||||
现在来增加一些实现了 `Draw` trait 的类型。我们将提供 `Button` 类型,再一次重申,真正实现 GUI 库超出了本书的范畴,所以 `draw` 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 `Button` 结构体可能会拥有 `width`、`height`和`label`字段,如列表 17-7 所示:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
@ -131,12 +132,11 @@ impl Draw for Button {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 17-7: 实一个现了`Draw` trait 的 `Button` 结构体</span>
|
||||
<span class="caption">列表 17-7: 一个实现了`Draw` trait 的 `Button` 结构体</span>
|
||||
|
||||
在 `Button` 上的 `width`、`height` 和 `label` 会和其他组件不同,比如 `TextField` 可能有 `width`、`height`,
|
||||
`label` 以及 `placeholder` 字段。每个我们可以在屏幕上绘制的类型都会实现 `Draw` trait,在 `draw` 方法中使用不同的代码,定义了如何绘制 `Button`。除了 `Draw` trait,`Button` 也可能有一个 `impl` 块,包含按钮被点击时的响应方法。这类方法不适用于 `TextField` 这样的类型。
|
||||
在 `Button` 上的 `width`、`height` 和 `label` 字段会和其他组件不同,比如 `TextField` 可能有 `width`、`height`、`label` 以及 `placeholder` 字段。每一个我们希望能在屏幕上绘制的类型都会使用不同的代码来实现 `Draw` trait 的 `draw` 方法,来定义如何绘制像这里的 `Button` 类型(并不包含任何实际的 GUI 代码,这超出了本章的范畴)。除了实现 `Draw` trait 之外,`Button` 还可能有另一个包含按钮点击如何响应的方法的 `impl` 块。这类方法并不适用于像 `TextField` 这样的类型。
|
||||
|
||||
假定我们的库的用户相要实现一个包含 `width`、`height` 和 `options` 的 `SelectBox` 结构体。同时也在 `SelectBox` 类型上实现了 `Draw` trait,如 列表 17-8 所示:
|
||||
一些库的使用者决定实现一个包含 `width`、`height`和`options` 字段的结构体 `SelectBox`。并也为其实现了 `Draw` trait,如列表 17-8 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -157,10 +157,9 @@ impl Draw for SelectBox {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 17-8: 另外一个 crate 中,在 `SelectBox` 结构体上使用 `rust_gui` 和实现了`Draw` trait
|
||||
</span>
|
||||
<span class="caption">列表 17-8: 在另一个使用 `rust_gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait</span>
|
||||
|
||||
库的用户现在可以在他们的 `main` 函数中创建一个 `Screen` 实例,然后把自身放入 `Box<T>` 变成 trait 对象,向 screen 增加 `SelectBox` 和 `Button`。他们可以在这个 `Screen` 实例上调用 `run` 方法,这又会调用每个组件的 `draw` 方法。 列表 17-9 展示了实现:
|
||||
库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例,并通过将 `SelectBox` 和 `Button` 放入 `Box<T>` 转变为 trait 对象来将它们放入屏幕实例。接着可以调用 `Screen` 的 `run` 方法,它会调用每个组件的 `draw` 方法。列表 17-9 展示了这个实现:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -191,16 +190,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型
|
||||
</span>
|
||||
<span class="caption">列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型的值</span>
|
||||
|
||||
虽然我们不知道哪一天会有人增加 `SelectBox` 类型,但是我们的 `Screen` 能够操作 `SelectBox` 并绘制它,因为 `SelectBox` 实现了 `Draw` 类型,这意味着它实现了 `draw` 方法。
|
||||
即使我们不知道何时何人会增加 `SelectBox` 类型,`Screen` 的实现能够操作`SelectBox` 并绘制它,因为 `SelectBox` 实现了 `Draw` trait,这意味着它实现了 `draw` 方法。
|
||||
|
||||
只关心值的响应,而不关心其具体类型,这类似于动态类型语言中的 *duck typing*:如果它像鸭子一样走路,像鸭子一样叫,那么它就是只鸭子!在 Listing 17-5 `Screen` 的 `run` 方法实现中,`run` 不需要知道每个组件的具体类型。它也不检查组件是 `Button` 还是 `SelectBox` 的实例,只管调用组件的 `draw` 方法。通过指定 `Box<Draw>` 作为 `components` 列表中元素的类型,我们约束了 `Screen` 需要这些实现了 `draw` 方法的值。
|
||||
只关心值所反映的信息而不是值的具体类型,这类似于动态类型语言中称为**鸭子类型**(*duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在列表 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件实例是 `Button` 或者是`SelectBox`,它只是调用组件上的 `draw` 方法。通过指定 `Box<Draw>` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。
|
||||
|
||||
Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。
|
||||
使用 trait 对象和 Rust 类型系统来使用鸭子类型的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。
|
||||
|
||||
比如,列表 17-10 展示了当我们创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
|
||||
例如,列表 17-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -219,9 +217,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 17-10: 尝试使用一种没有实现 trait 对象的类型
|
||||
|
||||
</span>
|
||||
<span class="caption">列表 17-10: 尝试使用一种没有实现 trait 对象的 trait 的类型</span>
|
||||
|
||||
我们会遇到这个错误,因为 `String` 没有实现 `Draw` trait:
|
||||
|
||||
@ -236,15 +232,15 @@ error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
|
||||
= note: required for the cast to the object type `Draw`
|
||||
```
|
||||
|
||||
这个错误告诉我们,要么传入 `Screen` 需要的类型,要么在 `String` 上实现 `Draw`,以便 `Screen` 调用它的 `draw` 方法。
|
||||
这告诉了我们,要么是我们传递了并不希望传递给 `Screen` 的类型并应该提供其他类型,要么应该在 `String` 上实现 `Draw` 以便 `Screen` 可以调用其上的 `draw`。
|
||||
|
||||
### Trait 对象执行动态分发
|
||||
### trait 对象执行动态分发
|
||||
|
||||
回忆一下第十章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 *static dispatch*:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。
|
||||
回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行**静态分发**(*static dispatch*):当方法被调用时,伴随方法调用的代码在编译时就被确定了,同时寻找这些代码是非常快速的。
|
||||
|
||||
当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 *dynamic dispatch*,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。
|
||||
当使用 trait 对象时,编译器并不进行单态化,因为并不知道所有可能会使用这些代码的类型。相反,Rust 记录当方法被调用时可能会用到的代码,并在运行时计算出特定方法调用时所需的代码。这被称为**动态分发**(*dynamic dispatch*),进行这种代码搜寻是有运行时开销的。动态分发也阻止编译有选择的内联方法的代码,这会禁用一些优化。尽管在编写和支持代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。
|
||||
|
||||
### Trait 对象需要对象安全
|
||||
### Trait 对象要求对象安全
|
||||
|
||||
<!-- Liz: we're conflicted on including this section. Not being able to use a
|
||||
trait as a trait object because of object safety is something that
|
||||
@ -256,16 +252,16 @@ objects. Clone is an example of one. You'll get errors that will let you know
|
||||
if a trait can't be a trait object, look up object safety if you're interested
|
||||
in the details"? Thanks! /Carol -->
|
||||
|
||||
不是所有的 trait 都可以被放进 trait 对象中; 只有*对象安全的*(*object safe*)trait 才可以这样做. 一个 trait 只有同时满足如下两点时才被认为是对象安全的:
|
||||
不是所有的 trait 都可以被放进 trait 对象中;只有**对象安全**(*object safe*)的 trait 才可以。 一个 trait 只有同时满足如下两点时才被认为是对象安全的:
|
||||
|
||||
* 该 trait 要求 `Self` 不是 `Sized`;
|
||||
* 该 trait 的所有方法都是对象安全的;
|
||||
* trait 不要求 `Self` 是 `Sized` 的
|
||||
* 所有的 trait 方法都是对象安全的
|
||||
|
||||
`Self` 是一个类型的别名关键字,它表示当前正被实现的 trait 类型或者是方法所属的类型. `Sized`是一个像在第十六章中介绍的`Send`和`Sync`那样的标记 trait, 在编译时它会自动被放进大小确定的类型里,比如`i32`和引用. 大小不确定的类型有 slice(`[T]`)和 trait 对象.
|
||||
`Self` 关键字是我们要实现 trait 或方法的类型的别名。`Sized` 是一个类似第十六章中介绍的 `Send` 和 `Sync` 那样的标记 trait。`Sized` 会自动为在编译时有已知大小的类型实现,比如 `i32` 和引用。包括 slice (`[T]`)和 trait 对象这样的没有已知大小的类型则没有。
|
||||
|
||||
`Sized` 是一个默认会被绑定到所有常规类型参数的内隐 trait. Rust 中要求一个类型是`Sized`的最具可用性的用法是让`Sized`成为一个默认的 trait 绑定,这样我们就可以在大多数的常规的用法中不去写 `T: Sized` 了. 如果我们想在切片(slice)中使用一个 trait, 我们需要取消对`Sized`的 trait 绑定, 我们只需制定`T: ?Sized`作为 trait 绑定.
|
||||
`Sized` 是一个所有泛型参数类型默认的隐含 trait bound。Rust 中大部分实用的操作都要求类型是 `Sized` 的,所以将 `Sized` 作为默认 trait bound 要求,就可以不必在每一次使用泛型时编写 `T: Sized` 了。然而,如果想要使用在 slice 上使用 trait,则需要去掉 `Sized` trait bound,可以通过指定 `T: ?Sized` 作为 trait bound 来做到这一点。
|
||||
|
||||
默认绑定到 `Self: ?Sized` 的 trait 可以被实现到是 `Sized` 或非 `Sized` 的类型上. 如果我们创建一个不绑定 `Self: ?Sized` 的 trait `Foo`,它看上去应该像这样:
|
||||
trait 有一个默认的 bound `Self: ?Sized`,这意味着他们可以在是或者不是 `Sized` 的类型上实现。如果创建了一个去掉了 `Self: ?Sized` bound 的 trait `Foo`,它可能看起来像这样:
|
||||
|
||||
```rust
|
||||
trait Foo: Sized {
|
||||
@ -273,21 +269,21 @@ trait Foo: Sized {
|
||||
}
|
||||
```
|
||||
|
||||
Trait `Sized`现在就是 trait `Foo`的一个*超级 trait*(*supertrait*), 也就是说 trait `Foo` 需要实现了 `Foo` 的类型(即`Self`)是`Sized`. 我们将在第十九章中更详细的介绍超 trait(supertrait).
|
||||
trait `Sized` 现在就是 trait `Foo` 的**父 trait**(*supertrait*)了,也就意味着 trait `Foo` 要求实现 `Foo` 的类型(也就是 `Self`)是 `Sized` 的。我们将在第十九章中更详细的介绍父 trait。
|
||||
|
||||
像`Foo`那样要求`Self`是`Sized`的 trait 不允许成为 trait 对象的原因是不可能为 trait 对象`Foo`实现 trait `Foo`: trait 对象是无确定大小的,但是 `Foo` 要求 `Self` 是 `Sized`. 一个类型不可能同时既是有大小的又是无确定大小的.
|
||||
像 `Foo` 这样要求 `Self` 是 `Sized` 的 trait 不被允许成为 trait 对象的原因是,不可能为 trait 对象实现 `Foo` trait:trait 对象不是 `Sized` 的,但是 `Foo` 又要求 `Self` 是 `Sized` 的。一个类型不可能同时既是有确定大小的又是无确定大小的。
|
||||
|
||||
第二点说对象安全要求一个 trait 的所有方法必须是对象安全的. 一个对象安全的方法满足下列条件:
|
||||
关于第二条对象安全要求说到 trait 的所有方法都必须是对象安全的,一个对象安全的方法满足下列条件之一:
|
||||
|
||||
* 它要求 `Self` 是 `Sized` 或者
|
||||
* 它符合下面全部三点:
|
||||
* 它不包含任意类型的常规参数
|
||||
* 它的第一个参数必须是类型 `Self` 或一个引用到 `Self` 的类型(也就是说它必须是一个方法而非关联函数并且以 `self`、`&self` 或 `&mut self` 作为第一个参数)
|
||||
* 除了第一个参数外它不能在其它地方用 `Self` 作为方法的参数签名
|
||||
* 要求 `Self` 是 `Sized` 的,或者
|
||||
* 满足如下三点:
|
||||
* 必须不包含任何泛型类型参数
|
||||
* 其第一个参数必须是 `Self` 类型或者能解引用为 `Self` 的类型(也就是说它必须是一个方法而非关联函数,并且以 `self`、`&self` 或 `&mut self` 作为第一个参数)
|
||||
* 必须不能在方法签名中除第一个参数之外的地方使用 `Self`
|
||||
|
||||
虽然这些规则有一点形式化, 但是换个角度想一下: 如果你的方法在它的参数签名的其它地方也需要具体的 `Self` 类型参数, 但是一个对象又忘记了它的具体类型是什么, 这时该方法就无法使用被它忘记的原先的具体类型. 当该 trait 被使用时, 被具体类型参数填充的常规类型参数也是如此: 这个具体的类型就成了实现该 trait 的类型的某一部分, 如果使用一个 trait 对象时这个类型被抹掉了, 就没有办法知道该用什么类型来填充这个常规类型参数.
|
||||
虽然这些规则有一点形式化, 但是换个角度想一下:如果方法在它的签名的其他什么地方要求使用具体的 `Self` 类型,而一个对象又忘记了它具体的类型,这时方法就无法使用它遗忘的原始的具体类型了。当使用 trait 的泛型类型参数被放入具体类型参数时也是如此:这个具体的类型就成了实现该 trait 的类型的一部分。一旦这个类型因使用 trait 对象而被擦除掉了之后,就无法知道放入泛型类型参数的类型是什么了。
|
||||
|
||||
一个 trait 的方法不是对象安全的一个例子是标准库中的 `Clone` trait. `Clone` trait 的 `clone` 方法的参数签名是这样的:
|
||||
一个 trait 的方法不是对象安全的例子是标准库中的 `Clone` trait。`Clone` trait 的 `clone` 方法的参数签名看起来像这样:
|
||||
|
||||
```rust
|
||||
pub trait Clone {
|
||||
@ -295,21 +291,21 @@ pub trait Clone {
|
||||
}
|
||||
```
|
||||
|
||||
`String` 实现了 `Clone` trait, 当我们在一个 `String` 实例上调用 `clone` 方法时, 我们会得到一个 `String` 实例. 同样地, 如果我们在一个 `Vec` 实例上调用 `clone` 方法, 我们会得到一个 `Vec` 实例. `clone` 的参数签名需要知道 `Self` 是什么类型, 因为它需要返回这个类型.
|
||||
`String` 实现了 `Clone` trait,当在 `String` 实例上调用 `clone` 方法时会得到一个 `String` 实例。类似的,当调用 `Vec` 实例的 `clone` 方法会得到一个 `Vec` 实例。`clone` 的签名需要知道什么类型会代替 `Self`,因为这是它的返回值。
|
||||
|
||||
如果我们像列表 17-3 中列出的 `Draw` trait 那样的 trait 上实现 `Clone`, 我们就不知道 `Self` 将会是一个 `Button`, 一个 `SelectBox`, 或者是其它的在将来要实现 `Draw` trait 的类型.
|
||||
如果尝试在像列表 17-3 中 `Draw` 那样的 trait 上实现 `Clone`,就无法知道 `Self` 将会是 `Button`、`SelectBox` 亦或是将来会实现 `Draw` trait 的其他什么类型。
|
||||
|
||||
如果你做了违反 trait 对象的对象安全性规则的事情, 编译器将会告诉你. 比如, 如果你实现在列表 17-4 中列出的 `Screen` 结构, 你想让该结构像这样持有实现了 `Clone` trait 的类型而不是 `Draw` trait:
|
||||
如果尝试做一些违反有关 trait 对象但违反对象安全规则的事情,编译器会提示你。例如,如果尝试实现列表 17-4 中的 `Screen` 结构体来存放实现了 `Clone` trait 而不是 `Draw` trait 的类型,像这样:
|
||||
|
||||
```rust
|
||||
```rust,ignore
|
||||
pub struct Screen {
|
||||
pub components: Vec<Box<Clone>>,
|
||||
}
|
||||
```
|
||||
|
||||
我们将会得到下面的错误:
|
||||
将会得到如下错误:
|
||||
|
||||
```text
|
||||
```
|
||||
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
|
||||
-->
|
||||
|
|
||||
|
@ -1,4 +1,4 @@
|
||||
# 高级特性
|
||||
# 高级特征
|
||||
|
||||
我们已经走得很远了! 现在我们已经学了使用Rust时99%的需要学习的内容. 在我们做第20章中的项目之前, 让我们来谈谈你可能会遇到的最后的1%的问题. 你可以随便跳过本章, 当你在实作中遇到这些问题时再回过头来学习也无妨; 我们将学习的在这里列出的特性在某些特定的情况下非常有用. 我们不想舍弃这些特性, 但你用到它们的时候确实不多.
|
||||
|
||||
|
@ -218,7 +218,7 @@ note: ...so that the reference type `&'a T` does not outlive the data it points
|
||||
|
||||
因为 `T` 可以是任意类型,`T` 自身也可能是一个引用,或者是一个存放了一个或多个引用的类型,而他们各自可能有着不同的生命周期。Rust 不能确认 `T` 会与 `'a` 存活的一样久。
|
||||
|
||||
幸运的是,Rust 在这种情况下给出了如何指定生命周期 bound 的好建议:
|
||||
幸运的是,Rust 提供了这个情况下如何指定生命周期 bound 的有用建议:
|
||||
|
||||
```
|
||||
consider adding an explicit lifetime bound `T: 'a` so that the reference type
|
||||
@ -241,7 +241,7 @@ struct StaticRef<T: 'static>(&'static T);
|
||||
|
||||
<span class="caption">列表 19-18:在 `T` 上增加 `'static` 生命周期 bound 来限制 `T` 为只拥有 `'static` 引用或没有引用的类型</span>
|
||||
|
||||
没有任何引用的类型就计为 `T: 'static`。因为 `'static` 意味着引用必须同整个程序存活得一样长,一个不包含引用的类型满足所有引用都与程序存活得一样长的标准(因为他们没有引用)。可以这样理解:如果借用检查器关心的是引用能存活多久,那么没有引用的类型与有引用且引用能一直存活的类型并没有真正的区别;对于确定引用是否比其所引用的值存活得较短的目的来说两者是一样的。
|
||||
没有任何引用的类型被算作 `T: 'static`。因为 `'static` 意味着引用必须同整个程序存活的一样长,一个不包含引用的类型满足所有引用都与程序存活的一样长的标准(因为他们没有引用)。可以这样理解:如果借用检查器关心的是引用是否存活的够久,那么没有引用的类型与有永远存在的引用的类型并没有真正的区别;对于确定引用是否比其所引用的值存活得较短的目的来说两者是一样的。
|
||||
|
||||
### trait 对象生命周期
|
||||
|
||||
|
426
src/ch19-03-advanced-traits.md
Normal file
426
src/ch19-03-advanced-traits.md
Normal file
@ -0,0 +1,426 @@
|
||||
## 高级 trait
|
||||
|
||||
> [ch19-03-advanced-traits.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-03-advanced-traits.md)
|
||||
> <br>
|
||||
> commit f8727711388b28eb2f5c852dd83fdbe6d22ab9bb
|
||||
|
||||
第十章讲到了 trait,不过就像生命周期,我们并没有涉及所有的细节。现在我们更加了解 Rust 了,可以深入理解本质了。
|
||||
|
||||
### 关联类型
|
||||
|
||||
**关联类型**(*associated types*)是一个将类型占位符与 trait 相关联的方法,如此 trait 的方法定义的签名中就可以使用这些占位符类型。实现 trait 的类型将会在特定实现中指定所用的具体类型。
|
||||
|
||||
本章描述的大部分内容都非常少见。关联类型则比较适中;他们比本书其他的内容要少见,不过比本章很多的内容要更常见。
|
||||
|
||||
一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。第十三章曾提到过 `Iterator` trait 的定义如列表 19-20 所示:
|
||||
|
||||
```rust
|
||||
pub trait Iterator {
|
||||
type Item;
|
||||
fn next(&mut self) -> Option<Self::Item>;
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-20:`Iterator` trait 的定义中带有关联类型 `Item`</span>
|
||||
|
||||
这就是说 `Iterator` trait 有一个关联类型 `Item`。`Item` 是一个占位类型,同时 `next` 方法会返回 `Option<Self::Item>` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,而 `next` 方法会返回一个 `Option` 包含无论实现者指定的何种类型的值。
|
||||
|
||||
#### 关联类型 vs 泛型
|
||||
|
||||
当在列表 13-6 中在 `Counter` 结构体上实现 `Iterator` trait 时,将 `Item` 的类型指定为 `u32`:
|
||||
|
||||
```rust
|
||||
impl Iterator for Counter {
|
||||
type Item = u32;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
```
|
||||
|
||||
这感觉类似于泛型。那么为什么 `Iterator` trait 不定义为如列表 19-21 所示这样呢?
|
||||
|
||||
```rust
|
||||
pub trait Iterator<T> {
|
||||
fn next(&mut self) -> Option<T>;
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-21:一个使用泛型的 `Iterator` trait 假象定义</span>
|
||||
|
||||
区别是在列表 19-21 的定义中,我们也可以实现 `Iterator<String> for Counter`,或者任何其他类型,这样就可以有多个 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 `Counter` 的 `next` 方法时,必须提供类型注解来表明希望使用 `Iterator` 的哪一个实现。
|
||||
|
||||
通过关联类型,不能多次实现 trait。使用列表 19-20 中这个 `Iterator` 的具体定义,只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。
|
||||
|
||||
当 trait 使用关联类型时不必指定泛型参数的好处也在另外一些方面得到体现。考虑一下列表 19-22 中定义的两个 trait。他们都必须处理一个包含一些节点和边的图结构。`GGraph` 定义为使用泛型,而 `AGraph` 定义为使用关联类型:
|
||||
|
||||
```rust
|
||||
trait GGraph<Node, Edge> {
|
||||
// methods would go here
|
||||
}
|
||||
|
||||
trait AGraph {
|
||||
type Node;
|
||||
type Edge;
|
||||
|
||||
// methods would go here
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-22:两个图 trait 定义,`GGraph` 使用泛型而 `AGraph` 使用关联类型代表 `Node` 和 `Edge`</span>
|
||||
|
||||
比如说想要是实现一个计算任何实现了图 trait 的类型中两个节点之间距离的函数。对于使用泛型的 `GGraph` trait 来说,`distance` 函数的签名看起来应该如列表 19-23 所示:
|
||||
|
||||
```rust
|
||||
# trait GGraph<Node, Edge> {}
|
||||
#
|
||||
fn distance<N, E, G: GGraph<N, E>>(graph: &G, start: &N, end: &N) -> u32 {
|
||||
# 0
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-23:`distance` 函数的签名,它使用 `GGraph` trait 并必须指定所有的泛型参数</span>
|
||||
|
||||
函数需要指定泛型参数 `N`、`E` 和 `G`,其中 `G` 拥有以 `N` 类型作为 `Node` 和 `E` 类型作为 `Edge` 的 `GGraph` trait 作为 trait bound。即便 `distance` 函数无需指定边的类型,我们也强制声明了 `E` 参数,因为需要使用 `GGraph` trait 而这样一来需要指定 `Edge` 的类型。
|
||||
|
||||
与此相对,列表 19-24 中的 `distance` 定义使用列表 19-22 中带有关联类型的 `AGraph` trait:
|
||||
|
||||
```rust
|
||||
# trait AGraph {
|
||||
# type Node;
|
||||
# type Edge;
|
||||
# }
|
||||
#
|
||||
fn distance<G: AGraph>(graph: &G, start: &G::Node, end: &G::Node) -> u32 {
|
||||
# 0
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-24:`distance` 函数的签名,它使用 trait `AGraph` 和关联类型 `Node`</span>
|
||||
|
||||
这样就清楚多了。只需指定一个泛型参数 `G`,带有 `AGraph` trait bound。因为 `distance` 完全不需要使用 `Edge` 类型,无需每次都指定它。为了使用 `AGraph` 的关联类型 `Node`,可以指定为 `G::Node`。
|
||||
|
||||
#### 带有关联类型的 trait 对象
|
||||
|
||||
你可能会好奇为什么不在列表 19-23 和 19-24 的 `distance` 函数中使用 trait 对象。当使用 trait 对象时使用泛型 `GGraph` trait 的 `distance` 函数的签名确实跟准确了一些:
|
||||
|
||||
```rust
|
||||
# trait GGraph<Node, Edge> {}
|
||||
#
|
||||
fn distance<N, E>(graph: &GGraph<N, E>, start: &N, end: &N) -> u32 {
|
||||
# 0
|
||||
}
|
||||
```
|
||||
|
||||
与列表 19-24 相比较可能更显公平。不过依然需要指定 `Edge` 类型,这意味着列表 19-24 仍更为合适,因为无需指定并不需要的类型。
|
||||
|
||||
不可能改变列表 19-24 来对图使用 trait 对象,因为这样就无法引用 `AGraph` trait 中的关联类型。
|
||||
|
||||
但是一般而言使用带有关联类型的 trait 的 trait 对象是可能;列表 19-25 展示了一个函数 `traverse` ,它无需在其他参数中使用关联类型。然而这种情况必须指定关联类型的具体类型。这里选择接受以 `usize` 作为 `Node` 和以两个 `usize` 值的元组作为 `Edge` 的实现了 `AGraph` trait 的类型:
|
||||
|
||||
```rust
|
||||
# trait AGraph {
|
||||
# type Node;
|
||||
# type Edge;
|
||||
# }
|
||||
#
|
||||
fn traverse(graph: &AGraph<Node=usize, Edge=(usize, usize)>) {}
|
||||
```
|
||||
|
||||
虽然 trait 对象意味着无需在编译时就知道 `graph` 参数的具体类型,但是我们确实需要在 `traverse` 函数中通过具体的关联类型来限制 `AGraph` trait 的使用。如果不提供这样的限制,Rust 将不能计算出用哪个 `impl` 来匹配这个 trait 对象,因为关联类型可以作为方法签名的一部分,Rust 需要在虚函数表中寻找他们。
|
||||
|
||||
### 运算符重载和默认类型参数
|
||||
|
||||
`<PlaceholderType=ConcreteType>` 语法也可以以另一种方式使用:用来指定泛型的默认类型。这种情况的一个非常好的例子是用于运算符重载。
|
||||
|
||||
Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。例如,列表 19-25 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
use std::ops::Add;
|
||||
|
||||
#[derive(Debug,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 });
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-25:实现 `Add` 来重载 `Point` 的 `+` 运算符</span>
|
||||
|
||||
这里实现了 `add` 方法将两个 `Point` 实例的 `x` 值和 `y` 值分别相加来创建一个新的 `Point`。`Add` trait 有一个叫做 `Output` 的关联类型,它用来决定 `add` 方法的返回值类型。
|
||||
|
||||
让我们更仔细的看看 `Add` trait。这里是其定义:
|
||||
|
||||
```rust
|
||||
trait Add<RHS=Self> {
|
||||
type Output;
|
||||
|
||||
fn add(self, rhs: RHS) -> Self::Output;
|
||||
}
|
||||
```
|
||||
|
||||
这看来应该很熟悉;这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 `RHS=Self`:这个语法叫做**默认类型参数**(*default type parameters*)。`RHS` 是一个泛型参数(“right hand side” 的缩写),它用于 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `RHS` 的具体类型,`RHS` 的类型将是默认的 `Self` 类型(在其上实现 `Add` 的类型)。
|
||||
|
||||
让我们看看另一个实现了 `Add` trait 的例子。想象一下我们拥有两个存放不同的单元值的结构体,`Millimeters` 和 `Meters`。可以如列表 19-26 所示那样用不同的方式为 `Millimeters` 实现 `Add` trait:
|
||||
|
||||
```rust
|
||||
use std::ops::Add;
|
||||
|
||||
struct Millimeters(u32);
|
||||
struct Meters(u32);
|
||||
|
||||
impl Add for Millimeters {
|
||||
type Output = Millimeters;
|
||||
|
||||
fn add(self, other: Millimeters) -> Millimeters {
|
||||
Millimeters(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Meters> for Millimeters {
|
||||
type Output = Millimeters;
|
||||
|
||||
fn add(self, other: Meters) -> Millimeters {
|
||||
Millimeters(self.0 + (other.0 * 1000))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-26:在 `Millimeters` 上实现 `Add`,以能够将`Millimeters` 与 `Millimeters` 相加和将 `Millimeters` 与 `Meters` 相加</span>
|
||||
|
||||
如果将 `Millimeters` 与其他 `Millimeters` 相加,则无需为 `Add` 参数化 `RHS` 类型,因为默认的 `Self` 正是我们希望的。如果希望实现 `Millimeters` 与 `Meters` 相加,那么需要声明为 `impl Add<Meters>` 来设定 `RHS` 类型参数的值。
|
||||
|
||||
默认参数类型主要用于如下两个方面:
|
||||
|
||||
1. 扩展类型而不破坏现有代码。
|
||||
2. 允许以一种大部分用户都不需要的方法进行自定义。
|
||||
|
||||
`Add` trait 就是第二个目的一个例子:大部分时候你会将两个相似的类型相加。在 `Add` trait 定义中使用默认类型参数使得实现 trait 变得更容易,因为大部分时候无需指定这额外的参数。换句话说,这样就去掉了一些实现的样板代码。
|
||||
|
||||
第一个目的是相似的,但过程是反过来的:因为现有 trait 实现并没有指定类型参数,如果需要为现有 trait 增加类型参数,为其提供一个默认值将允许我们在不破坏现有实现代码的基础上扩展 trait 的功能。
|
||||
|
||||
### 完全限定语法与消歧义
|
||||
|
||||
Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至也可以直接在类型上实现相同名称的方法!那么为了能使用相同的名称调用每一个方法,需要告诉 Rust 我们希望使用哪个方法。考虑一下列表 19-27 中的代码,trait `Foo` 和 `Bar` 都拥有方法 `f`,并在结构体 `Baz` 上实现了这两个 trait,结构体也有一个叫做 `f` 的方法:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
trait Foo {
|
||||
fn f(&self);
|
||||
}
|
||||
|
||||
trait Bar {
|
||||
fn f(&self);
|
||||
}
|
||||
|
||||
struct Baz;
|
||||
|
||||
impl Foo for Baz {
|
||||
fn f(&self) { println!("Baz’s impl of Foo"); }
|
||||
}
|
||||
|
||||
impl Bar for Baz {
|
||||
fn f(&self) { println!("Baz’s impl of Bar"); }
|
||||
}
|
||||
|
||||
impl Baz {
|
||||
fn f(&self) { println!("Baz's impl"); }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let b = Baz;
|
||||
b.f();
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-27:实现两个拥有相同名称的方法的 trait,同时还有直接定义于结构体的(同名)方法</span>
|
||||
|
||||
对于 `Baz` 的 `Foo` trait 中方法 `f` 的实现,它打印出 `Baz's impl of Foo`。对于 `Baz` 的 `Bar` trait 中方法 `f` 的实现,它打印出 `Baz's impl of Bar`。直接定义于 `Baz` 的 `f` 实现打印出 `Baz's impl`。当调用 `b.f()` 时会发生什么呢?在这个例子中,Rust 总是会使用直接定义于 `Baz` 的实现并打印出 `Baz's impl`。
|
||||
|
||||
为了能够调用 `Foo` 和 `Baz` 中的 `f` 方法而不是直接定义于 `Baz` 的 `f` 实现,则需要使用**完全限定语法**(*fully qualified syntax*)来调用方法。它像这样工作:对于任何类似如下的方法调用:
|
||||
|
||||
```rust
|
||||
receiver.method(args);
|
||||
```
|
||||
|
||||
可以像这样使用完全限定的方法调用:
|
||||
|
||||
```rust
|
||||
<Type as Trait>::method(receiver, args);
|
||||
```
|
||||
|
||||
所以为了消歧义并能够调用列表 19-27 中所有的 `f` 方法,需要在尖括号中指定每个希望 `Baz` 作为的 trait,接着使用双冒号,接着传递 `Baz` 实例作为第一个参数并调用 `f` 方法。列表 19-28 展示了如何调用 `Foo` 中的 `f`,和 `Bar` 中与 `b` 中的 `f`:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
# trait Foo {
|
||||
# fn f(&self);
|
||||
# }
|
||||
# trait Bar {
|
||||
# fn f(&self);
|
||||
# }
|
||||
# struct Baz;
|
||||
# impl Foo for Baz {
|
||||
# fn f(&self) { println!("Baz’s impl of Foo"); }
|
||||
# }
|
||||
# impl Bar for Baz {
|
||||
# fn f(&self) { println!("Baz’s impl of Bar"); }
|
||||
# }
|
||||
# impl Baz {
|
||||
# fn f(&self) { println!("Baz's impl"); }
|
||||
# }
|
||||
#
|
||||
fn main() {
|
||||
let b = Baz;
|
||||
b.f();
|
||||
<Baz as Foo>::f(&b);
|
||||
<Baz as Bar>::f(&b);
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-28:使用完全限定语法调用作为`Foo` 和 `Bar` trait 一部分的 `f` 方法</span>
|
||||
|
||||
这会打印出:
|
||||
|
||||
```
|
||||
Baz's impl
|
||||
Baz’s impl of Foo
|
||||
Baz’s impl of Bar
|
||||
```
|
||||
|
||||
只在存在歧义时才需要 `Type as` 部分,只有需要 `Type as` 时才需要 `<>` 部分。所以如果在作用域中只有定义于 `Baz` 和 `Baz` 上实现的 `Foo` trait 的 `f` 方法的话,则可以使用 `Foo::f(&b)` 调用 `Foo` 中的 `f` 方法,因为无需与 `Bar` trait 相区别。
|
||||
|
||||
也可以使用 `Baz::f(&b)` 调用直接定义于 `Baz` 上的 `f` 方法,不过因为这个定义是在调用 `b.f()` 时默认使用的,并不要求调用此方法时使用完全限定的名称。
|
||||
|
||||
### 父 trait 用于在另一个 trait 中使用某 trait 的功能
|
||||
|
||||
有时我们希望当实现某 trait 时依赖另一个 trait 也被实现,如此这个 trait 就可以使用其他 trait 的功能。这个所需的 trait 是我们实现的 trait 的**父(超) trait**(*supertrait*)。
|
||||
|
||||
例如,加入我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会打印出带有星号框的值。也就是说,如果 `Point` 实现了 `Display` 并返回 `(x, y)`,调用以 1 作为 `x` 和 3 作为 `y` 的 `Point` 实例的 `outline_print` 会显示如下:
|
||||
|
||||
```
|
||||
**********
|
||||
* *
|
||||
* (1, 3) *
|
||||
* *
|
||||
**********
|
||||
```
|
||||
|
||||
在 `outline_print` 的实现中,因为希望能够使用 `Display` trait 的功能,则需要说明 `OutlinePrint` 只能用于同时也实现了 `Display` 并提供了 `OutlinePrint` 需要的功能的类型。可以在 trait 定义中指定 `OutlinePrint: Display` 来做到这一点。这类似于为 trait 增加 trait bound。列表 19-29 展示了一个 `OutlinePrint` trait 的实现:
|
||||
|
||||
```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));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-29:实现 `OutlinePrint` trait,它要求来自 `Display` 的功能</span>
|
||||
|
||||
因为指定了 `OutlinePrint` 需要 `Display` trait,则可以在 `outline_print` 中使用 `to_string`(`to_string` 会为任何实现 `Display` 的类型自动实现)。如果不在 trait 名后增加 `: Display` 并尝试在 `outline_print` 中使用 `to_string`,则会得到一个错误说在当前作用域中没有找到用于 `&Self` 类型的方法 `to_string`。
|
||||
|
||||
如果尝试在一个没有实现 `Display` 的类型上实现 `OutlinePrint`,比如 `Point` 结构体:
|
||||
|
||||
```rust
|
||||
# trait OutlinePrint {}
|
||||
struct Point {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
impl OutlinePrint for Point {}
|
||||
```
|
||||
|
||||
则会得到一个错误说 `Display` 没有被实现而 `Display` 被 `OutlinePrint` 所需要:
|
||||
|
||||
```
|
||||
error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
|
||||
--> src/main.rs:20:6
|
||||
|
|
||||
20 | impl OutlinePrint for Point {}
|
||||
| ^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for
|
||||
`Point`
|
||||
|
|
||||
= note: `Point` cannot be formatted with the default formatter; try using
|
||||
`:?` instead if you are using a format string
|
||||
= note: required by `OutlinePrint`
|
||||
```
|
||||
|
||||
一旦在 `Point` 上实现 `Display` 并满足 `OutlinePrint` 要求的限制,比如这样:
|
||||
|
||||
```rust
|
||||
# struct Point {
|
||||
# x: i32,
|
||||
# y: i32,
|
||||
# }
|
||||
#
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Display for Point {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
那么在 `Point` 实现 `OutlinePrint` trait 将能成功编译并可以在 `Point` 实例上调用 `outline_print` 来显示位于星号框中的点的值。
|
||||
|
||||
### newtype 模式用以在外部类型上实现外部 trait
|
||||
|
||||
在第十章中,我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用**newtype 模式**(*newtype pattern*),它涉及到使用一个元组结构体来创建一个新类型,它带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。“Newtype” 是一个源自 Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚。这个封装类型在编译时被省略了。
|
||||
|
||||
例如,如果想要在 `Vec` 上实现 `Display`,可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体。接着可以如列表 19-30 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```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("hello"), String::from("world")]);
|
||||
println!("w = {}", w);
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">列表 19-30:创建 `Wrapper` 类型封装 `Vec<String>` 以便实现 `Display`</span>
|
||||
|
||||
`Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,接着就可以使用 `Wrapper` 中 `Display` 的功能了。
|
||||
|
||||
此方法的缺点是因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,如 `push`、`pop` 等等,并代理到 `self.0` 上以便可以将 `Wrapper` 完全当作 `Vec` 处理。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现第十五章讲到的 `Deref` trait 并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法,比如为了限制封装类型的行为,则必须自行实现所需的方法。
|
||||
|
||||
上面便是 newtype 模式如何与 trait 结合使用的;还有一个不涉及 trait 的实用模式。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方法上来吧。
|
6
src/ch19-04-advanced-types.md
Normal file
6
src/ch19-04-advanced-types.md
Normal file
@ -0,0 +1,6 @@
|
||||
## 高级类型
|
||||
|
||||
> [ch19-04-advanced-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-04-advanced-types.md)
|
||||
> <br>
|
||||
> commit e084e1773667c8eae28d9aab6d4939348eec0092
|
||||
|
Loading…
Reference in New Issue
Block a user