mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-14 04:41:49 +08:00
修改错误语法 '他们' -> '它们'
This commit is contained in:
parent
99503422f8
commit
76aa7d738a
@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
### 非运算符符号
|
### 非运算符符号
|
||||||
|
|
||||||
下面的列表中包含了所有和运算符不一样功能的符号;也就是说,他们并不像函数调用或方法调用一样表现。
|
下面的列表中包含了所有和运算符不一样功能的符号;也就是说,它们并不像函数调用或方法调用一样表现。
|
||||||
|
|
||||||
表 B-2 展示了以其自身出现以及出现在合法其他各个地方的符号。
|
表 B-2 展示了以其自身出现以及出现在合法其他各个地方的符号。
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
|
|||||||
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-03-string-move/src/main.rs:here}}
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-03-string-move/src/main.rs:here}}
|
||||||
```
|
```
|
||||||
|
|
||||||
这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个 `s1` 的拷贝并绑定到 `s2` 上。不过,事实上并不完全是这样。
|
这看起来与上面的代码非常类似,所以我们可能会假设它们的运行方式也是类似的:也就是说,第二行可能会生成一个 `s1` 的拷贝并绑定到 `s2` 上。不过,事实上并不完全是这样。
|
||||||
|
|
||||||
看看图 4-1 以了解 `String` 的底层会发生什么。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
|
看看图 4-1 以了解 `String` 的底层会发生什么。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ src="img/trpl04-03.svg" class="center" style="width: 50%;" />
|
|||||||
|
|
||||||
<span class="caption">图 4-3:另一个 `s2 = s1` 时可能的内存表现,如果 Rust 同时也拷贝了堆上的数据的话</span>
|
<span class="caption">图 4-3:另一个 `s2 = s1` 时可能的内存表现,如果 Rust 同时也拷贝了堆上的数据的话</span>
|
||||||
|
|
||||||
之前我们提到过当变量离开作用域后,Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` 和 `s1` 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 **二次释放**(*double free*)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
|
之前我们提到过当变量离开作用域后,Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` 和 `s1` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 **二次释放**(*double free*)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
|
||||||
|
|
||||||
为了确保内存安全,在 `let s2 = s1;` 之后,Rust 认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生什么;这段代码不能运行:
|
为了确保内存安全,在 `let s2 = s1;` 之后,Rust 认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生什么;这段代码不能运行:
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ access the heap data." src="img/trpl04-04.svg" class="center" style="width:
|
|||||||
|
|
||||||
#### 只在栈上的数据:拷贝
|
#### 只在栈上的数据:拷贝
|
||||||
|
|
||||||
这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是示例 4-2 中的一部分:
|
这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,它们是示例 4-2 中的一部分:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-06-copy/src/main.rs:here}}
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-06-copy/src/main.rs:here}}
|
||||||
@ -237,7 +237,7 @@ Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用 `Co
|
|||||||
|
|
||||||
<span class="caption">示例 4-3:带有所有权和作用域注释的函数</span>
|
<span class="caption">示例 4-3:带有所有权和作用域注释的函数</span>
|
||||||
|
|
||||||
当尝试在调用 `takes_ownership` 后使用 `s` 时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 `main` 函数中添加使用 `s` 和 `x` 的代码来看看哪里能使用他们,以及所有权规则会在哪里阻止我们这么做。
|
当尝试在调用 `takes_ownership` 后使用 `s` 时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 `main` 函数中添加使用 `s` 和 `x` 的代码来看看哪里能使用它们,以及所有权规则会在哪里阻止我们这么做。
|
||||||
|
|
||||||
### 返回值与作用域
|
### 返回值与作用域
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 8612c4a5801b61f8d2e952f8bbbb444692ff1ec2
|
> commit 8612c4a5801b61f8d2e952f8bbbb444692ff1ec2
|
||||||
|
|
||||||
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。
|
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代它们为止。
|
||||||
|
|
||||||
使用 Cargo 新建一个叫做 *rectangles* 的二进制程序,它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积。示例 5-8 显示了位于项目的 *src/main.rs* 中的小程序,它刚刚好实现此功能:
|
使用 Cargo 新建一个叫做 *rectangles* 的二进制程序,它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积。示例 5-8 显示了位于项目的 *src/main.rs* 中的小程序,它刚刚好实现此功能:
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ Rust **确实** 包含了打印出调试信息的功能,不过我们必须为
|
|||||||
|
|
||||||
我们可以看到第一点输出来自 *src/main.rs* 第 10 行,我们正在调试表达式 `30 * scale`,其结果值是 `60`(为整数实现的 `Debug` 格式化是只打印它们的值)。在 *src/main.rs* 第 14 行 的 `dbg!` 调用输出 `&rect1` 的值,即 `Rectangle` 结构。这个输出使用了更为易读的 `Debug` 格式。当你试图弄清楚你的代码在做什么时,`dbg!` 宏可能真的很有帮助!
|
我们可以看到第一点输出来自 *src/main.rs* 第 10 行,我们正在调试表达式 `30 * scale`,其结果值是 `60`(为整数实现的 `Debug` 格式化是只打印它们的值)。在 *src/main.rs* 第 14 行 的 `dbg!` 调用输出 `&rect1` 的值,即 `Rectangle` 结构。这个输出使用了更为易读的 `Debug` 格式。当你试图弄清楚你的代码在做什么时,`dbg!` 宏可能真的很有帮助!
|
||||||
|
|
||||||
除了 `Debug` trait,Rust 还为我们提供了很多可以通过 `derive` 属性来使用的 trait,他们可以为我们的自定义类型增加实用的行为。[附录 C][app-c] 中列出了这些 trait 和行为。第十章会介绍如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。除了 `derive` 之外,还有很多属性;更多信息请参见 [Rust Reference][attributes] 的 Attributes 部分。
|
除了 `Debug` trait,Rust 还为我们提供了很多可以通过 `derive` 属性来使用的 trait,它们可以为我们的自定义类型增加实用的行为。[附录 C][app-c] 中列出了这些 trait 和行为。第十章会介绍如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。除了 `derive` 之外,还有很多属性;更多信息请参见 [Rust Reference][attributes] 的 Attributes 部分。
|
||||||
|
|
||||||
我们的 `area` 函数是非常特殊的,它只计算长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的 `area` **方法** 中。
|
我们的 `area` 函数是非常特殊的,它只计算长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的 `area` **方法** 中。
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ backyard
|
|||||||
|
|
||||||
我们定义一个模块,是以 `mod` 关键字为起始,然后指定模块的名字(本例中叫做 `front_of_house`),并且用花括号包围模块的主体。在模块内,我们还可以定义其他的模块,就像本例中的 `hosting` 和 `serving` 模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。
|
我们定义一个模块,是以 `mod` 关键字为起始,然后指定模块的名字(本例中叫做 `front_of_house`),并且用花括号包围模块的主体。在模块内,我们还可以定义其他的模块,就像本例中的 `hosting` 和 `serving` 模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。
|
||||||
|
|
||||||
通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义。程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性。
|
通过使用模块,我们可以将相关的定义分组到一起,并指出它们为什么相关。程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义。程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性。
|
||||||
|
|
||||||
在前面我们提到了,`src/main.rs` 和 `src/lib.rs` 叫做 crate 根。之所以这样叫它们是因为这两个文件的内容都分别在 crate 模块结构的根组成了一个名为 `crate` 的模块,该结构被称为 *模块树*(*module tree*)。
|
在前面我们提到了,`src/main.rs` 和 `src/lib.rs` 叫做 crate 根。之所以这样叫它们是因为这两个文件的内容都分别在 crate 模块结构的根组成了一个名为 `crate` 的模块,该结构被称为 *模块树*(*module tree*)。
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
错误信息说 `hosting` 模块是私有的。换句话说,我们拥有 `hosting` 模块和 `add_to_waitlist` 函数的的正确路径,但是 Rust 不让我们使用,因为它不能访问私有片段。在 Rust 中,默认所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的。如果希望创建一个私有函数或结构体,你可以将其放入一个模块。
|
错误信息说 `hosting` 模块是私有的。换句话说,我们拥有 `hosting` 模块和 `add_to_waitlist` 函数的的正确路径,但是 Rust 不让我们使用,因为它不能访问私有片段。在 Rust 中,默认所有项(函数、方法、结构体、枚举、模块和常量)对父模块都是私有的。如果希望创建一个私有函数或结构体,你可以将其放入一个模块。
|
||||||
|
|
||||||
父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。继续拿餐馆作比喻,把私有性规则想象成餐馆的后台办公室:餐馆内的事务对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅并在其中做任何事情。
|
父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用它们父模块中的项。这是因为子模块封装并隐藏了它们的实现详情,但是子模块可以看到它们定义的上下文。继续拿餐馆作比喻,把私有性规则想象成餐馆的后台办公室:餐馆内的事务对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅并在其中做任何事情。
|
||||||
|
|
||||||
Rust 选择以这种方式来实现模块系统功能,因此默认隐藏内部实现细节。这样一来,你就知道可以更改内部代码的哪些部分而不会破坏外部代码。不过 Rust 也确实提供了通过使用 `pub` 关键字来创建公共项,使子模块的内部部分暴露给上级模块。
|
Rust 选择以这种方式来实现模块系统功能,因此默认隐藏内部实现细节。这样一来,你就知道可以更改内部代码的哪些部分而不会破坏外部代码。不过 Rust 也确实提供了通过使用 `pub` 关键字来创建公共项,使子模块的内部部分暴露给上级模块。
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ Rust 选择以这种方式来实现模块系统功能,因此默认隐藏内部
|
|||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 7-7: 为 `mod hosting`
|
<span class="caption">示例 7-7: 为 `mod hosting`
|
||||||
和 `fn add_to_waitlist` 添加 `pub` 关键字使他们可以在
|
和 `fn add_to_waitlist` 添加 `pub` 关键字使它们可以在
|
||||||
`eat_at_restaurant` 函数中被调用</span>
|
`eat_at_restaurant` 函数中被调用</span>
|
||||||
|
|
||||||
现在代码可以编译通过了!为了了解为何增加 `pub` 关键字使得我们可以在 `add_to_waitlist` 中调用这些路径与私有性规则有关,让我们看看绝对路径和相对路径。
|
现在代码可以编译通过了!为了了解为何增加 `pub` 关键字使得我们可以在 `add_to_waitlist` 中调用这些路径与私有性规则有关,让我们看看绝对路径和相对路径。
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
>
|
>
|
||||||
> 如果你对同一模块同时使用这两种路径风格,会得到一个编译错误。在同一项目中的不同模块混用不同的路径风格是允许的,不过这会使他人感到疑惑。
|
> 如果你对同一模块同时使用这两种路径风格,会得到一个编译错误。在同一项目中的不同模块混用不同的路径风格是允许的,不过这会使他人感到疑惑。
|
||||||
>
|
>
|
||||||
> 使用 *mod.rs* 这一文件名的风格的主要缺点是会导致项目中出现很多 *mod.rs* 文件,当你在编辑器中同时打开他们时会感到疑惑。
|
> 使用 *mod.rs* 这一文件名的风格的主要缺点是会导致项目中出现很多 *mod.rs* 文件,当你在编辑器中同时打开它们时会感到疑惑。
|
||||||
|
|
||||||
我们将各个模块的代码移动到独立文件了,同时模块树依旧相同。`eat_at_restaurant` 中的函数调用也无需修改继续保持有效,即便其定义存在于不同的文件中。这个技巧让你可以在模块代码增长时,将它们移动到新文件中。
|
我们将各个模块的代码移动到独立文件了,同时模块树依旧相同。`eat_at_restaurant` 中的函数调用也无需修改继续保持有效,即便其定义存在于不同的文件中。这个技巧让你可以在模块代码增长时,将它们移动到新文件中。
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素
|
|||||||
|
|
||||||
<span class="caption">示例 8-7:通过 `for` 循环遍历 vector 的元素并打印</span>
|
<span class="caption">示例 8-7:通过 `for` 循环遍历 vector 的元素并打印</span>
|
||||||
|
|
||||||
我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们。示例 8-8 中的 `for` 循环会给每一个元素加 `50`:
|
我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变它们。示例 8-8 中的 `for` 循环会给每一个元素加 `50`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-08/src/main.rs:here}}
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-08/src/main.rs:here}}
|
||||||
|
@ -8,4 +8,4 @@
|
|||||||
|
|
||||||
Rust 将错误分为两大类:**可恢复的**(*recoverable*)和 **不可恢复的**(*unrecoverable*)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
|
Rust 将错误分为两大类:**可恢复的**(*recoverable*)和 **不可恢复的**(*unrecoverable*)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
|
||||||
|
|
||||||
大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理他们。Rust 没有异常。相反,它有 `Result<T, E>` 类型,用于处理可恢复的错误,还有 `panic!` 宏,在程序遇到不可恢复的错误时停止执行。本章首先介绍 `panic!` 调用,接着会讲到如何返回 `Result<T, E>`。此外,我们将探讨在决定是尝试从错误中恢复还是停止执行时的注意事项。
|
大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理它们。Rust 没有异常。相反,它有 `Result<T, E>` 类型,用于处理可恢复的错误,还有 `panic!` 宏,在程序遇到不可恢复的错误时停止执行。本章首先介绍 `panic!` 调用,接着会讲到如何返回 `Result<T, E>`。此外,我们将探讨在决定是尝试从错误中恢复还是停止执行时的注意事项。
|
||||||
|
@ -254,7 +254,7 @@ src/libcore/result.rs:906:4
|
|||||||
|
|
||||||
`main` 函数也可以返回任何实现了 [`std::process::Termination` trait][termination] 的类型,它包含了一个返回 `ExitCode` 的 `report` 函数。请查阅标准库文档了解更多为自定义类型实现 `Termination` trait 的细节。
|
`main` 函数也可以返回任何实现了 [`std::process::Termination` trait][termination] 的类型,它包含了一个返回 `ExitCode` 的 `report` 函数。请查阅标准库文档了解更多为自定义类型实现 `Termination` trait 的细节。
|
||||||
|
|
||||||
现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到他们各自适合哪些场景的话题了。
|
现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到它们各自适合哪些场景的话题了。
|
||||||
|
|
||||||
[handle_failure]: ch02-00-guessing-game-tutorial.html#使用-result-类型来处理潜在的错误
|
[handle_failure]: ch02-00-guessing-game-tutorial.html#使用-result-类型来处理潜在的错误
|
||||||
[trait-objects]: ch17-02-trait-objects.html#为使用不同类型的值而设计的-trait-对象
|
[trait-objects]: ch17-02-trait-objects.html#为使用不同类型的值而设计的-trait-对象
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。
|
那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。
|
||||||
|
|
||||||
在一些类似示例、原型代码(prototype code)和测试中,panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下,panic 是合适的。章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
|
在一些类似示例、原型代码(prototype code)和测试中,panic 比返回 `Result` 更为合适,不过它们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下,panic 是合适的。章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
|
||||||
|
|
||||||
### 示例、代码原型和测试都非常适合 panic
|
### 示例、代码原型和测试都非常适合 panic
|
||||||
|
|
||||||
@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到格式错误的数据,或者 HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
|
然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到格式错误的数据,或者 HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
|
||||||
|
|
||||||
当你的代码在进行一个使用无效值进行调用时可能将用户置于风险中的操作时,代码应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的 **程序员** 修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
|
当你的代码在进行一个使用无效值进行调用时可能将用户置于风险中的操作时,代码应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):它们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的 **程序员** 修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
|
||||||
|
|
||||||
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个并不是 `Option` 的类型,则程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some` 和 `None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。
|
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个并不是 `Option` 的类型,则程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some` 和 `None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。
|
||||||
|
|
||||||
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
然而,这并不是一个理想的解决方案:如果让程序仅仅处理 1 到 100 之间的值是一个绝对需要满足的要求,而且程序中的很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
|
然而,这并不是一个理想的解决方案:如果让程序仅仅处理 1 到 100 之间的值是一个绝对需要满足的要求,而且程序中的很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
|
||||||
|
|
||||||
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-13 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
|
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信它们接收到的值。示例 9-13 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../listings/ch09-error-handling/listing-09-13/src/main.rs:here}}
|
{{#include ../listings/ch09-error-handling/listing-09-13/src/main.rs:here}}
|
||||||
@ -76,6 +76,6 @@
|
|||||||
|
|
||||||
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!` 和 `Result` 将会使你的代码在面对不可避免的错误时显得更加可靠。
|
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!` 和 `Result` 将会使你的代码在面对不可避免的错误时显得更加可靠。
|
||||||
|
|
||||||
现在我们已经见识过了标准库中 `Option` 和 `Result` 泛型枚举的能力了,在下一章让我们聊聊泛型是如何工作的,以及如何在你的代码中使用他们。
|
现在我们已经见识过了标准库中 `Option` 和 `Result` 泛型枚举的能力了,在下一章让我们聊聊泛型是如何工作的,以及如何在你的代码中使用它们。
|
||||||
|
|
||||||
[encoding]: ch17-03-oo-design-patterns.html#将状态和行为编码为类型
|
[encoding]: ch17-03-oo-design-patterns.html#将状态和行为编码为类型
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 4aa96a3d20570f868bd20e8e3e865b047284be30
|
> commit 4aa96a3d20570f868bd20e8e3e865b047284be30
|
||||||
|
|
||||||
每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
|
每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如它们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道它们在这里实际上代表什么。
|
||||||
|
|
||||||
函数可以获取一些不同于 `i32` 或 `String` 这样具体类型的泛型参数,就像一个获取未知类型值的函数可以对多种具体类型的值运行同一段代码一样。事实上我们已经使用过第六章的 `Option<T>`,第八章的 `Vec<T>` 和 `HashMap<K, V>`,以及第九章的 `Result<T, E>` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法!
|
函数可以获取一些不同于 `i32` 或 `String` 这样具体类型的泛型参数,就像一个获取未知类型值的函数可以对多种具体类型的值运行同一段代码一样。事实上我们已经使用过第六章的 `Option<T>`,第八章的 `Vec<T>` 和 `HashMap<K, V>`,以及第九章的 `Result<T, E>` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法!
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ fn largest<T>(list: &[T]) -> &T {
|
|||||||
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-07/src/main.rs}}
|
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-07/src/main.rs}}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 10-7:字段 `x` 和 `y` 的类型必须相同,因为他们都有相同的泛型类型 `T`</span>
|
<span class="caption">示例 10-7:字段 `x` 和 `y` 的类型必须相同,因为它们都有相同的泛型类型 `T`</span>
|
||||||
|
|
||||||
在这个例子中,当把整型值 5 赋值给 `x` 时,就告诉了编译器这个 `Point<T>` 实例中的泛型 `T` 全是整型。接着指定 `y` 为浮点值 4.0,因为它`y`被定义为与 `x` 相同类型,所以将会得到一个像这样的类型不匹配错误:
|
在这个例子中,当把整型值 5 赋值给 `x` 时,就告诉了编译器这个 `Point<T>` 实例中的泛型 `T` 全是整型。接着指定 `y` 为浮点值 4.0,因为它`y`被定义为与 `x` 相同类型,所以将会得到一个像这样的类型不匹配错误:
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ enum Result<T, E> {
|
|||||||
|
|
||||||
在 `main` 函数中,定义了一个有 `i32` 类型的 `x`(其值为 `5`)和 `f64` 的 `y`(其值为 `10.4`)的 `Point`。`p2` 则是一个有着字符串 slice 类型的 `x`(其值为 `"Hello"`)和 `char` 类型的 `y`(其值为`c`)的 `Point`。在 `p1` 上以 `p2` 作为参数调用 `mixup` 会返回一个 `p3`,它会有一个 `i32` 类型的 `x`,因为 `x` 来自 `p1`,并拥有一个 `char` 类型的 `y`,因为 `y` 来自 `p2`。`println!` 会打印出 `p3.x = 5, p3.y = c`。
|
在 `main` 函数中,定义了一个有 `i32` 类型的 `x`(其值为 `5`)和 `f64` 的 `y`(其值为 `10.4`)的 `Point`。`p2` 则是一个有着字符串 slice 类型的 `x`(其值为 `"Hello"`)和 `char` 类型的 `y`(其值为`c`)的 `Point`。在 `p1` 上以 `p2` 作为参数调用 `mixup` 会返回一个 `p3`,它会有一个 `i32` 类型的 `x`,因为 `x` 来自 `p1`,并拥有一个 `char` 类型的 `y`,因为 `y` 来自 `p2`。`println!` 会打印出 `p3.x = 5, p3.y = c`。
|
||||||
|
|
||||||
这个例子的目的是展示一些泛型通过 `impl` 声明而另一些通过方法定义声明的情况。这里泛型参数 `X1` 和 `Y1` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `X2` 和 `Y2` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。
|
这个例子的目的是展示一些泛型通过 `impl` 声明而另一些通过方法定义声明的情况。这里泛型参数 `X1` 和 `Y1` 声明于 `impl` 之后,因为它们与结构体定义相对应。而泛型参数 `X2` 和 `Y2` 声明于 `fn mixup` 之后,因为它们只是相对于方法本身的。
|
||||||
|
|
||||||
### 泛型代码的性能
|
### 泛型代码的性能
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ enum Result<T, E> {
|
|||||||
|
|
||||||
Rust 通过在编译时进行泛型代码的 **单态化**(*monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
|
Rust 通过在编译时进行泛型代码的 **单态化**(*monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
|
||||||
|
|
||||||
这这个过程中,编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
|
在这个过程中,编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
|
||||||
|
|
||||||
让我们看看这如何用于标准库中的 `Option` 枚举:
|
让我们看看这如何用于标准库中的 `Option` 枚举:
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
|
|||||||
|
|
||||||
<span class="caption">示例 10-15:根据 trait bound 在泛型上有条件的实现方法</span>
|
<span class="caption">示例 10-15:根据 trait bound 在泛型上有条件的实现方法</span>
|
||||||
|
|
||||||
也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 *blanket implementations*,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 `Display` trait 的类型实现了 `ToString` trait。这个 `impl` 块看起来像这样:
|
也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 *blanket implementations*,它们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 `Display` trait 的类型实现了 `ToString` trait。这个 `impl` 块看起来像这样:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
impl<T: Display> ToString for T {
|
impl<T: Display> ToString for T {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
生命周期是另一类我们已经使用过的泛型。不同于确保类型有期望的行为,生命周期确保引用如预期一直有效。
|
生命周期是另一类我们已经使用过的泛型。不同于确保类型有期望的行为,生命周期确保引用如预期一直有效。
|
||||||
|
|
||||||
当在第四章讨论 [“引用和借用”][references-and-borrowing] 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
|
当在第四章讨论 [“引用和借用”][references-and-borrowing] 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明它们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
|
||||||
|
|
||||||
生命周期注解甚至不是一个大部分语言都有的概念,所以这可能感觉起来有些陌生。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。
|
生命周期注解甚至不是一个大部分语言都有的概念,所以这可能感觉起来有些陌生。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作
|
|||||||
|
|
||||||
这段代码能够编译并会产生我们希望得到的示例 10-19 中的 `main` 函数的结果。
|
这段代码能够编译并会产生我们希望得到的示例 10-19 中的 `main` 函数的结果。
|
||||||
|
|
||||||
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。它的实际含义是 `longest` 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致。这些关系就是我们希望 Rust 分析代码时所使用的。
|
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,它们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。它的实际含义是 `longest` 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致。这些关系就是我们希望 Rust 分析代码时所使用的。
|
||||||
|
|
||||||
记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作
|
|||||||
|
|
||||||
出现的问题是 `result` 在 `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
|
出现的问题是 `result` 在 `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
|
||||||
|
|
||||||
综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
|
综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦它们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
|
||||||
|
|
||||||
### 结构体定义中的生命周期注解
|
### 结构体定义中的生命周期注解
|
||||||
|
|
||||||
@ -306,7 +306,7 @@ fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
|
|||||||
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:3rd}}
|
{{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:3rd}}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 `&self` 和 `announcement` 他们各自的生命周期。接着,因为其中一个参数是 `&self`,返回值类型被赋予了 `&self` 的生命周期,这样所有的生命周期都被计算出来了。
|
这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 `&self` 和 `announcement` 它们各自的生命周期。接着,因为其中一个参数是 `&self`,返回值类型被赋予了 `&self` 的生命周期,这样所有的生命周期都被计算出来了。
|
||||||
|
|
||||||
### 静态生命周期
|
### 静态生命周期
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ Rust 中的测试函数是用来验证非测试代码是否是按照期望的方
|
|||||||
|
|
||||||
每次使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这个模块提供了一个编写测试的模板,为此每次开始新项目时不必去查找测试函数的具体结构和语法了。因为这样当然你也可以额外增加任意多的测试函数以及测试模块!
|
每次使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这个模块提供了一个编写测试的模板,为此每次开始新项目时不必去查找测试函数的具体结构和语法了。因为这样当然你也可以额外增加任意多的测试函数以及测试模块!
|
||||||
|
|
||||||
在实际编写测试代码之前,让我们先通过尝试那些自动生成的测试模版来探索测试是如何工作的。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。
|
在实际编写测试代码之前,让我们先通过尝试那些自动生成的测试模版来探索测试是如何工作的。接着,我们会写一些真正的测试,调用我们编写的代码并断言它们的行为的正确性。
|
||||||
|
|
||||||
让我们创建一个新的库项目 `adder`,它会将两个数字相加:
|
让我们创建一个新的库项目 `adder`,它会将两个数字相加:
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ Cargo 编译并运行了测试。可以看到 `running 1 test` 这一行。下
|
|||||||
|
|
||||||
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个求值为布尔值的参数。如果值是 `true`,`assert!` 什么也不做,同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。`assert!` 宏帮助我们检查代码是否以期望的方式运行。
|
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个求值为布尔值的参数。如果值是 `true`,`assert!` 什么也不做,同时测试会通过。如果值为 `false`,`assert!` 调用 `panic!` 宏,这会导致测试失败。`assert!` 宏帮助我们检查代码是否以期望的方式运行。
|
||||||
|
|
||||||
回忆一下第五章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。
|
回忆一下第五章中,示例 5-15 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用它们。将它们放进 *src/lib.rs* 并使用 `assert!` 宏编写一些测试。
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ Cargo 编译并运行了测试。可以看到 `running 1 test` 这一行。下
|
|||||||
|
|
||||||
### 使用 `assert_eq!` 和 `assert_ne!` 宏来测试相等
|
### 使用 `assert_eq!` 和 `assert_ne!` 宏来测试相等
|
||||||
|
|
||||||
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作 —— `assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是打印导致 `false` 的两个值。
|
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作 —— `assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时它们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是打印导致 `false` 的两个值。
|
||||||
|
|
||||||
示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数。
|
示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数。
|
||||||
|
|
||||||
@ -194,11 +194,11 @@ Cargo 编译并运行了测试。可以看到 `running 1 test` 这一行。下
|
|||||||
|
|
||||||
测试捕获到了 bug!`it_adds_two` 测试失败,错误信息告诉我们断言失败了,它告诉我们 `` assertion failed: `(left == right)` `` 以及 `left` 和 `right` 的值是什么。这个错误信息有助于我们开始调试:它说 `assert_eq!` 的 `left` 参数是 `4`,而 `right` 参数,也就是 `add_two(2)` 的结果,是 `5`。可以想象当有很多测试在运行时这些信息是多么的有用。
|
测试捕获到了 bug!`it_adds_two` 测试失败,错误信息告诉我们断言失败了,它告诉我们 `` assertion failed: `(left == right)` `` 以及 `left` 和 `right` 的值是什么。这个错误信息有助于我们开始调试:它说 `assert_eq!` 的 `left` 参数是 `4`,而 `right` 参数,也就是 `add_two(2)` 的结果,是 `5`。可以想象当有很多测试在运行时这些信息是多么的有用。
|
||||||
|
|
||||||
需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数被称为 `expected` 和 `actual`,而且指定参数的顺序非常重要。然而在 Rust 中,他们则叫做 `left` 和 `right`,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 `assert_eq!(add_two(2), 4)`,这时失败信息仍同样是 `` assertion failed: `(left == right)` ``。
|
需要注意的是,在一些语言和测试框架中,断言两个值相等的函数的参数被称为 `expected` 和 `actual`,而且指定参数的顺序非常重要。然而在 Rust 中,它们则叫做 `left` 和 `right`,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成 `assert_eq!(add_two(2), 4)`,这时失败信息仍同样是 `` assertion failed: `(left == right)` ``。
|
||||||
|
|
||||||
`assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。
|
`assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。
|
||||||
|
|
||||||
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C [“可派生 trait”][derivable-traits] 中有更多关于这些和其他派生 trait 的详细信息。
|
`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言它们的值是否相等。需要实现 `Debug` 才能在断言失败时打印它们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C [“可派生 trait”][derivable-traits] 中有更多关于这些和其他派生 trait 的详细信息。
|
||||||
|
|
||||||
### 自定义失败信息
|
### 自定义失败信息
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ Cargo 编译并运行了测试。可以看到 `running 1 test` 这一行。下
|
|||||||
|
|
||||||
这个程序的需求还没有被确定,因此问候文本开头的 `Hello` 文本很可能会改变。然而我们并不想在需求改变时不得不更新测试,所以相比检查 `greeting` 函数返回的确切值,我们将仅仅断言输出的文本中包含输入参数。
|
这个程序的需求还没有被确定,因此问候文本开头的 `Hello` 文本很可能会改变。然而我们并不想在需求改变时不得不更新测试,所以相比检查 `greeting` 函数返回的确切值,我们将仅仅断言输出的文本中包含输入参数。
|
||||||
|
|
||||||
让我们通过将 `greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的:
|
让我们通过将 `greeting` 改为不包含 `name` 在代码中引入一个 bug 来测试失败时是怎样的:
|
||||||
|
|
||||||
```rust,not_desired_behavior,noplayground
|
```rust,not_desired_behavior,noplayground
|
||||||
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/src/lib.rs:here}}
|
{{#rustdoc_include ../listings/ch11-writing-automated-tests/no-listing-06-greeter-with-bug/src/lib.rs:here}}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 34314c10f699cc882d4e0b06f2a24bd37a5435f2
|
> commit 34314c10f699cc882d4e0b06f2a24bd37a5435f2
|
||||||
|
|
||||||
就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。`cargo test` 产生的二进制文件的默认行为是并发运行所有的测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。不过可以指定命令行参数来改变 `cargo test` 的默认行为。
|
就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。`cargo test` 产生的二进制文件的默认行为是并发运行所有的测试,并截获测试运行过程中产生的输出,阻止它们被显示出来,使得阅读测试结果相关的内容变得更容易。不过可以指定命令行参数来改变 `cargo test` 的默认行为。
|
||||||
|
|
||||||
可以将一部分命令行参数传递给 `cargo test`,而将另外一部分传递给生成的测试二进制文件。为了分隔这两种参数,需要先列出传递给 `cargo test` 的参数,接着是分隔符 `--`,再之后是传递给测试二进制文件的参数。运行 `cargo test --help` 会提示 `cargo test` 的有关参数,而运行 `cargo test -- --help` 可以提示在分隔符之后使用的有关参数。
|
可以将一部分命令行参数传递给 `cargo test`,而将另外一部分传递给生成的测试二进制文件。为了分隔这两种参数,需要先列出传递给 `cargo test` 的参数,接着是分隔符 `--`,再之后是传递给测试二进制文件的参数。运行 `cargo test --help` 会提示 `cargo test` 的有关参数,而运行 `cargo test -- --help` 可以提示在分隔符之后使用的有关参数。
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ $ cargo test -- --show-output
|
|||||||
|
|
||||||
### 忽略某些测试
|
### 忽略某些测试
|
||||||
|
|
||||||
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除他们。虽然可以通过参数列举出所有希望运行的测试来做到,也可以使用 `ignore` 属性来标记耗时的测试并排除他们,如下所示:
|
有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 `cargo test` 的时候希望能排除它们。虽然可以通过参数列举出所有希望运行的测试来做到,也可以使用 `ignore` 属性来标记耗时的测试并排除它们,如下所示:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
### 单元测试
|
### 单元测试
|
||||||
|
|
||||||
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确地验证某个单元的代码功能是否符合预期。单元测试与他们要测试的代码共同存放在位于 *src* 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 `tests` 模块,并使用 `cfg(test)` 标注模块。
|
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确地验证某个单元的代码功能是否符合预期。单元测试与它们要测试的代码共同存放在位于 *src* 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 `tests` 模块,并使用 `cfg(test)` 标注模块。
|
||||||
|
|
||||||
#### 测试模块和 `#[cfg(test)]`
|
#### 测试模块和 `#[cfg(test)]`
|
||||||
|
|
||||||
测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 注解。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。
|
测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 注解。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定它们不应该被包含进编译结果中。
|
||||||
|
|
||||||
回忆本章第一部分新建的 `adder` 项目,Cargo 为我们生成了如下代码:
|
回忆本章第一部分新建的 `adder` 项目,Cargo 为我们生成了如下代码:
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ adder
|
|||||||
|
|
||||||
随着集成测试的增加,你可能希望在 `tests` 目录创建更多文件以便更好地组织它们,例如根据测试的功能来将测试分组。如前所述,*tests* 目录中的每一个文件都被编译成一个单独的 crate,这有助于创建独立的作用域,以便更接近于最终用户使用你的 crate 的方式。但这意味着,*tests* 目录中的文件的行为,和你在第七章中学习如何将代码分为模块和文件时,学到的 *src* 中的文件的行为不一样。
|
随着集成测试的增加,你可能希望在 `tests` 目录创建更多文件以便更好地组织它们,例如根据测试的功能来将测试分组。如前所述,*tests* 目录中的每一个文件都被编译成一个单独的 crate,这有助于创建独立的作用域,以便更接近于最终用户使用你的 crate 的方式。但这意味着,*tests* 目录中的文件的行为,和你在第七章中学习如何将代码分为模块和文件时,学到的 *src* 中的文件的行为不一样。
|
||||||
|
|
||||||
当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第七章 [“将模块移动到其他文件”][separating-modules-into-files] 部分的步骤将他们提取到一个通用的模块中时, *tests* 目录中文件行为的不同就会凸显出来。例如,如果我们可以创建 一个*tests/common.rs* 文件并创建一个名叫 `setup` 的函数,我们希望这个函数能被多个测试文件的测试函数调用:
|
当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第七章 [“将模块移动到其他文件”][separating-modules-into-files] 部分的步骤将它们提取到一个通用的模块中时, *tests* 目录中文件行为的不同就会凸显出来。例如,如果我们可以创建 一个*tests/common.rs* 文件并创建一个名叫 `setup` 的函数,我们希望这个函数能被多个测试文件的测试函数调用:
|
||||||
|
|
||||||
<span class="filename">文件名:tests/common.rs</span>
|
<span class="filename">文件名:tests/common.rs</span>
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使
|
|||||||
- 合理的使用 trait 和生命周期([第十章][ch10])
|
- 合理的使用 trait 和生命周期([第十章][ch10])
|
||||||
- 测试([第十一章][ch11])
|
- 测试([第十一章][ch11])
|
||||||
|
|
||||||
另外还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在 [第十三章][ch13] 和 [第十七章][ch17] 中详细介绍。
|
另外还会简要的讲到闭包、迭代器和 trait 对象,它们分别会在 [第十三章][ch13] 和 [第十七章][ch17] 中详细介绍。
|
||||||
|
|
||||||
[ch7]: ch07-00-managing-growing-projects-with-packages-crates-and-modules.html
|
[ch7]: ch07-00-managing-growing-projects-with-packages-crates-and-modules.html
|
||||||
[ch8]: ch08-00-common-collections.html
|
[ch8]: ch08-00-common-collections.html
|
||||||
|
@ -74,7 +74,7 @@ $ cargo run -- searchstring example-filename.txt
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-02/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-02/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略他们并开始增加读取文件功能。
|
好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略它们并开始增加读取文件功能。
|
||||||
|
|
||||||
[ch13]: ch13-00-functional-features.html
|
[ch13]: ch13-00-functional-features.html
|
||||||
[ch7-idiomatic-use]: ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#创建惯用的-use-路径
|
[ch7-idiomatic-use]: ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#创建惯用的-use-路径
|
||||||
|
@ -36,4 +36,4 @@
|
|||||||
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/output.txt}}
|
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:此时 `main` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。
|
好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:此时 `main` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复它们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 83788ff212a3281328e2f8f223ce9e0f69220b97
|
> commit 83788ff212a3281328e2f8f223ce9e0f69220b97
|
||||||
|
|
||||||
为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。
|
为了改善我们的程序这里有四个问题需要修复,而且它们都与程序的组织方式和如何处理潜在错误有关。第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。
|
||||||
|
|
||||||
这同时也关系到第二个问题:`query` 和 `file_path` 是程序中的配置变量,而像 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使他们的目的更明确了。
|
这同时也关系到第二个问题:`query` 和 `file_path` 是程序中的配置变量,而像 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使它们的目的更明确了。
|
||||||
|
|
||||||
第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `Should have been able to read the file`。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!
|
第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `Should have been able to read the file`。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!
|
||||||
|
|
||||||
@ -29,7 +29,7 @@
|
|||||||
* 调用 *lib.rs* 中的 `run` 函数
|
* 调用 *lib.rs* 中的 `run` 函数
|
||||||
* 如果 `run` 返回错误,则处理这个错误
|
* 如果 `run` 返回错误,则处理这个错误
|
||||||
|
|
||||||
这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试他们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序。
|
这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试它们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序。
|
||||||
|
|
||||||
### 提取参数解析器
|
### 提取参数解析器
|
||||||
|
|
||||||
@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
|
我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
|
||||||
|
|
||||||
另一个表明还有改进空间的迹象是 `parse_config` 名称的 `config` 部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。
|
另一个表明还有改进空间的迹象是 `parse_config` 名称的 `config` 部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及它们的目的。
|
||||||
|
|
||||||
> 注意:一些同学将这种在复杂类型更为合适的场景下使用基本类型的反模式称为 **基本类型偏执**(*primitive obsession*)。
|
> 注意:一些同学将这种在复杂类型更为合适的场景下使用基本类型的反模式称为 **基本类型偏执**(*primitive obsession*)。
|
||||||
|
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<span class="caption">示例 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例</span>
|
<span class="caption">示例 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例</span>
|
||||||
|
|
||||||
新定义的结构体 `Config` 中包含字段 `query` 和 `file_path`。
|
新定义的结构体 `Config` 中包含字段 `query` 和 `file_path`。
|
||||||
`parse_config` 的签名表明它现在返回一个 `Config` 值。在之前的 `parse_config` 函数体中,我们返回了引用 `args` 中 `String` 值的字符串 slice,现在我们定义 `Config` 来包含拥有所有权的 `String` 值。`main` 中的 `args` 变量是参数值的所有者并只允许 `parse_config` 函数借用他们,这意味着如果 `Config` 尝试获取 `args` 中值的所有权将违反 Rust 的借用规则。
|
`parse_config` 的签名表明它现在返回一个 `Config` 值。在之前的 `parse_config` 函数体中,我们返回了引用 `args` 中 `String` 值的字符串 slice,现在我们定义 `Config` 来包含拥有所有权的 `String` 值。`main` 中的 `args` 变量是参数值的所有者并只允许 `parse_config` 函数借用它们,这意味着如果 `Config` 尝试获取 `args` 中值的所有权将违反 Rust 的借用规则。
|
||||||
|
|
||||||
还有许多不同的方式可以处理 `String` 的数据,而最简单但有些不太高效的方式是调用这些值的 `clone` 方法。这会生成 `Config` 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
|
还有许多不同的方式可以处理 `String` 的数据,而最简单但有些不太高效的方式是调用这些值的 `clone` 方法。这会生成 `Config` 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
|
||||||
|
|
||||||
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
我们更新 `main` 将 `parse_config` 返回的 `Config` 实例放入变量 `config` 中,并将之前分别使用 `query` 和 `file_path` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。
|
我们更新 `main` 将 `parse_config` 返回的 `Config` 实例放入变量 `config` 中,并将之前分别使用 `query` 和 `file_path` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。
|
||||||
|
|
||||||
现在代码更明确的表现了我们的意图,`query` 和 `file_path` 是相关联的并且他们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找他们。
|
现在代码更明确的表现了我们的意图,`query` 和 `file_path` 是相关联的并且它们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找它们。
|
||||||
|
|
||||||
### 创建一个 `Config` 的构造函数
|
### 创建一个 `Config` 的构造函数
|
||||||
|
|
||||||
@ -142,9 +142,9 @@
|
|||||||
|
|
||||||
现在 `build` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是字符串字面值的类型,也是目前的错误信息。
|
现在 `build` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是字符串字面值的类型,也是目前的错误信息。
|
||||||
|
|
||||||
`new` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。
|
`build` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。
|
||||||
|
|
||||||
通过让 `Config::build` 返回一个 `Err` 值,这就允许 `main` 函数处理 `new` 函数返回的 `Result` 值并在出现错误的情况更明确的结束进程。
|
通过让 `Config::build` 返回一个 `Err` 值,这就允许 `main` 函数处理 `build` 函数返回的 `Result` 值并在出现错误的情况更明确的结束进程。
|
||||||
|
|
||||||
#### 调用 `Config::build` 并处理错误
|
#### 调用 `Config::build` 并处理错误
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在
|
|||||||
|
|
||||||
#### 处理 `main` 中 `run` 返回的错误
|
#### 处理 `main` 中 `run` 返回的错误
|
||||||
|
|
||||||
我们将检查错误并使用类似示例 12-10 中 `Config::build` 处理错误的技术来处理他们,不过有一些细微的不同:
|
我们将检查错误并使用类似示例 12-10 中 `Config::build` 处理错误的技术来处理它们,不过有一些细微的不同:
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在
|
|||||||
|
|
||||||
### 将代码拆分到库 crate
|
### 将代码拆分到库 crate
|
||||||
|
|
||||||
现在我们的 `minigrep` 项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试他们并拥有一个含有更少功能的 `main` 函数。
|
现在我们的 `minigrep` 项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试它们并拥有一个含有更少功能的 `main` 函数。
|
||||||
|
|
||||||
让我们将所有不是 `main` 函数的代码从 *src/main.rs* 移动到新文件 *src/lib.rs* 中:
|
让我们将所有不是 `main` 函数的代码从 *src/main.rs* 移动到新文件 *src/lib.rs* 中:
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
### 编写失败测试
|
### 编写失败测试
|
||||||
|
|
||||||
去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要他们了。接着我们会像 [第十一章][ch11-anatomy] 那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译:
|
去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要它们了。接着我们会像 [第十一章][ch11-anatomy] 那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
|||||||
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:here}}
|
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:here}}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 12-19:储存匹配的行以便可以返回他们</span>
|
<span class="caption">示例 12-19:储存匹配的行以便可以返回它们</span>
|
||||||
|
|
||||||
现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试:
|
现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试:
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 `"Duct tape."` 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
|
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 `"Duct tape."` 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
|
||||||
|
|
||||||
大小写 **不敏感** 搜索的新测试使用 `"rUsT"` 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,`"rUsT"` 查询应该包含带有一个大写 R 的 `"Rust:"` 还有 `"Trust me."` 这两行,即便他们与查询的大小写都不同。这个测试现在不能编译,因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试通过编译并失败时所做的那样。
|
大小写 **不敏感** 搜索的新测试使用 `"rUsT"` 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,`"rUsT"` 查询应该包含带有一个大写 R 的 `"Rust:"` 还有 `"Trust me."` 这两行,即便它们与查询的大小写都不同。这个测试现在不能编译,因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试通过编译并失败时所做的那样。
|
||||||
|
|
||||||
### 实现 `search_case_insensitive` 函数
|
### 实现 `search_case_insensitive` 函数
|
||||||
|
|
||||||
@ -32,7 +32,7 @@
|
|||||||
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-21/src/lib.rs:here}}
|
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-21/src/lib.rs:here}}
|
||||||
```
|
```
|
||||||
|
|
||||||
<span class="caption">示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将他们都转换为小写</span>
|
<span class="caption">示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将它们都转换为小写</span>
|
||||||
|
|
||||||
首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。虽然 `to_lowercase` 可以处理基本的 Unicode,但它不是 100% 准确。如果编写真实的程序的话,我们还需多做一些工作,不过这一部分是关于环境变量而不是 Unicode 的,所以这样就够了。
|
首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。虽然 `to_lowercase` 可以处理基本的 Unicode,但它不是 100% 准确。如果编写真实的程序的话,我们还需多做一些工作,不过这一部分是关于环境变量而不是 Unicode 的,所以这样就够了。
|
||||||
|
|
||||||
|
@ -13,6 +13,6 @@ Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著
|
|||||||
* **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构
|
* **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构
|
||||||
* **迭代器**(*Iterators*),一种处理元素序列的方式
|
* **迭代器**(*Iterators*),一种处理元素序列的方式
|
||||||
* 如何使用闭包和迭代器来改进第十二章的 I/O 项目。
|
* 如何使用闭包和迭代器来改进第十二章的 I/O 项目。
|
||||||
* 闭包和迭代器的性能。(**剧透警告:** 他们的速度超乎你的想象!)
|
* 闭包和迭代器的性能。(**剧透警告:** 它们的速度超乎你的想象!)
|
||||||
|
|
||||||
我们已经介绍了其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。因为掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。
|
我们已经介绍了其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。因为掌握闭包和迭代器是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解它们。
|
||||||
|
@ -36,7 +36,7 @@ Rust 的 **闭包**(*closures*)是可以保存在一个变量中或作为参
|
|||||||
|
|
||||||
### 闭包类型推断和注解
|
### 闭包类型推断和注解
|
||||||
|
|
||||||
函数与闭包还有更多区别。闭包并不总是要求像 `fn` 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格定义这些接口对保证所有人都对函数使用和返回值的类型理解一致是很重要的。与此相比,闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
|
函数与闭包还有更多区别。闭包并不总是要求像 `fn` 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为它们是暴露给用户的显式接口的一部分。严格定义这些接口对保证所有人都对函数使用和返回值的类型理解一致是很重要的。与此相比,闭包并不用于这样暴露在外的接口:它们储存在变量中并被使用,不用命名它们或暴露给库的用户调用。
|
||||||
|
|
||||||
闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠地推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样(同时也有编译器需要闭包类型注解的罕见情况)。
|
闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠地推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样(同时也有编译器需要闭包类型注解的罕见情况)。
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ let add_one_v4 = |x| x + 1 ;
|
|||||||
|
|
||||||
即使闭包体不严格需要所有权,如果希望强制闭包获取它用到的环境中值的所有权,可以在参数列表前使用 `move` 关键字。
|
即使闭包体不严格需要所有权,如果希望强制闭包获取它用到的环境中值的所有权,可以在参数列表前使用 `move` 关键字。
|
||||||
|
|
||||||
在将闭包传递到一个新的线程时这个技巧很有用,它可以移动数据所有权给新线程。我们将在 16 章讨论并发时详细讨论线程以及为什么你想要使用它们。现在我们先简单探讨用需要 `move` 关键字的闭包来生成新的线程。示例 13-6 修改了示例 13-4 以便在一个新的线程而非主线程中打印 vector:
|
在将闭包传递到一个新的线程时这个技巧很有用,它可以移动数据所有权给新线程。我们将在 16 章讨论并发时详细讨论线程以及为什么你想要使用它们。现在我们先简单探讨用 `move` 关键字的闭包来生成新的线程。示例 13-6 修改了示例 13-4 以便在一个新的线程而非主线程中打印 vector:
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ pub trait Iterator {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意这里有一个我们还未讲到的新语法:`type Item` 和 `Self::Item`,他们定义了 trait 的 **关联类型**(*associated type*)。第十九章会深入讲解关联类型,不过现在只需知道这段代码表明实现 `Iterator` trait 要求同时定义一个 `Item` 类型,这个 `Item` 类型被用作 `next` 方法的返回值类型。换句话说,`Item` 类型将是迭代器返回元素的类型。
|
注意这里有一个我们还未讲到的新语法:`type Item` 和 `Self::Item`,它们定义了 trait 的 **关联类型**(*associated type*)。第十九章会深入讲解关联类型,不过现在只需知道这段代码表明实现 `Iterator` trait 要求同时定义一个 `Item` 类型,这个 `Item` 类型被用作 `next` 方法的返回值类型。换句话说,`Item` 类型将是迭代器返回元素的类型。
|
||||||
|
|
||||||
`next` 是 `Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。
|
`next` 是 `Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ pub trait Iterator {
|
|||||||
|
|
||||||
`Iterator` trait 有一系列不同的由标准库提供默认实现的方法;你可以在 `Iterator` trait 的标准库 API 文档中找到所有这些方法。一些方法在其定义中调用了 `next` 方法,这也就是为什么在实现 `Iterator` trait 时要求实现 `next` 方法的原因。
|
`Iterator` trait 有一系列不同的由标准库提供默认实现的方法;你可以在 `Iterator` trait 的标准库 API 文档中找到所有这些方法。一些方法在其定义中调用了 `next` 方法,这也就是为什么在实现 `Iterator` trait 时要求实现 `next` 方法的原因。
|
||||||
|
|
||||||
这些调用 `next` 方法的方法被称为 **消费适配器**(*consuming adaptors*),因为调用他们会消耗迭代器。一个消费适配器的例子是 `sum` 方法。这个方法获取迭代器的所有权并反复调用 `next` 来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。示例 13-13 有一个展示 `sum` 方法使用的测试:
|
这些调用 `next` 方法的方法被称为 **消费适配器**(*consuming adaptors*),因为调用它们会消耗迭代器。一个消费适配器的例子是 `sum` 方法。这个方法获取迭代器的所有权并反复调用 `next` 来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。示例 13-13 有一个展示 `sum` 方法使用的测试:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ pub trait Iterator {
|
|||||||
|
|
||||||
### 产生其他迭代器的方法
|
### 产生其他迭代器的方法
|
||||||
|
|
||||||
`Iterator` trait 中定义了另一类方法,被称为 **迭代器适配器**(*iterator adaptors*),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
|
`Iterator` trait 中定义了另一类方法,被称为 **迭代器适配器**(*iterator adaptors*),它们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
|
||||||
|
|
||||||
示例 13-14 展示了一个调用迭代器适配器方法 `map` 的例子,该 `map` 方法使用闭包来调用每个元素以生成新的迭代器。这里的闭包创建了一个新的迭代器,对其中 vector 中的每个元素都被加 1。:
|
示例 13-14 展示了一个调用迭代器适配器方法 `map` 的例子,该 `map` 方法使用闭包来调用每个元素以生成新的迭代器。这里的闭包创建了一个新的迭代器,对其中 vector 中的每个元素都被加 1。:
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
### 使用迭代器并去掉 `clone`
|
### 使用迭代器并去掉 `clone`
|
||||||
|
|
||||||
在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-17 中重现了第十二章结尾示例 12-23 中 `Config::build` 函数的实现:
|
在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,它们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-17 中重现了第十二章结尾示例 12-23 中 `Config::build` 函数的实现:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 13-17:重现示例 12-23 的 `Config::build` 函数</span>
|
<span class="caption">示例 13-17:重现示例 12-23 的 `Config::build` 函数</span>
|
||||||
|
|
||||||
那时我们说过不必担心低效的 `clone` 调用了,因为将来可以对他们进行改进。好吧,就是现在!
|
那时我们说过不必担心低效的 `clone` 调用了,因为将来可以对它们进行改进。好吧,就是现在!
|
||||||
|
|
||||||
起初这里需要 `clone` 的原因是参数 `args` 中有一个 `String` 元素的 slice,而 `build` 函数并不拥有 `args`。为了能够返回 `Config` 实例的所有权,我们需要克隆 `Config` 中字段 `query` 和 `file_path` 的值,这样 `Config` 实例就能拥有这些值。
|
起初这里需要 `clone` 的原因是参数 `args` 中有一个 `String` 元素的 slice,而 `build` 函数并不拥有 `args`。为了能够返回 `Config` 实例的所有权,我们需要克隆 `Config` 中字段 `query` 和 `file_path` 的值,这样 `Config` 实例就能拥有这些值。
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
|
|||||||
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
|
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
|
||||||
```
|
```
|
||||||
|
|
||||||
结果迭代器版本还要稍微快一点!这里我们将不会查看性能测试的代码,我们的目的并不是为了证明他们是完全等同的,而是得出一个怎样比较这两种实现方式性能的基本思路。
|
结果迭代器版本还要稍微快一点!这里我们将不会查看性能测试的代码,我们的目的并不是为了证明它们是完全等同的,而是得出一个怎样比较这两种实现方式性能的基本思路。
|
||||||
|
|
||||||
对于一个更全面的性能测试,将会检查不同长度的文本、不同的搜索单词、不同长度的单词和所有其他的可变情况。这里所要表达的是:迭代器,作为一个高级的抽象,被编译成了与手写的底层代码大体一致性能代码。迭代器是 Rust 的 **零成本抽象**(*zero-cost abstractions*)之一,它意味着抽象并不会引入运行时开销,它与本贾尼·斯特劳斯特卢普(C++ 的设计和实现者)在 “Foundations of C++”(2012)中所定义的 **零开销**(*zero-overhead*)如出一辙:
|
对于一个更全面的性能测试,将会检查不同长度的文本、不同的搜索单词、不同长度的单词和所有其他的可变情况。这里所要表达的是:迭代器,作为一个高级的抽象,被编译成了与手写的底层代码大体一致性能代码。迭代器是 Rust 的 **零成本抽象**(*zero-cost abstractions*)之一,它意味着抽象并不会引入运行时开销,它与本贾尼·斯特劳斯特卢普(C++ 的设计和实现者)在 “Foundations of C++”(2012)中所定义的 **零开销**(*zero-overhead*)如出一辙:
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
|
|||||||
>
|
>
|
||||||
> - Bjarne Stroustrup "Foundations of C++"
|
> - Bjarne Stroustrup "Foundations of C++"
|
||||||
>
|
>
|
||||||
> 从整体来说,C++ 的实现遵循了零开销原则:你不需要的,无需为他们买单。更有甚者的是:你需要的时候,也不可能找到其他更好的代码了。
|
> 从整体来说,C++ 的实现遵循了零开销原则:你不需要的,无需为它们买单。更有甚者的是:你需要的时候,也不可能找到其他更好的代码了。
|
||||||
>
|
>
|
||||||
> - 本贾尼·斯特劳斯特卢普 "Foundations of C++"
|
> - 本贾尼·斯特劳斯特卢普 "Foundations of C++"
|
||||||
|
|
||||||
@ -46,10 +46,10 @@ for i in 12..buffer.len() {
|
|||||||
|
|
||||||
像音频解码器这样的程序通常最看重计算的性能。这里,我们创建了一个迭代器,使用了两个适配器,接着消费了其值。那么这段 Rust 代码将会被编译为什么样的汇编代码呢?好吧,在编写本书的这个时候,它被编译成与手写的相同的汇编代码。遍历 `coefficients` 的值完全用不到循环:Rust 知道这里会迭代 12 次,所以它“展开”(unroll)了循环。展开是一种移除循环控制代码的开销,并将循环迭代转换为程序中的重复的代码的优化。
|
像音频解码器这样的程序通常最看重计算的性能。这里,我们创建了一个迭代器,使用了两个适配器,接着消费了其值。那么这段 Rust 代码将会被编译为什么样的汇编代码呢?好吧,在编写本书的这个时候,它被编译成与手写的相同的汇编代码。遍历 `coefficients` 的值完全用不到循环:Rust 知道这里会迭代 12 次,所以它“展开”(unroll)了循环。展开是一种移除循环控制代码的开销,并将循环迭代转换为程序中的重复的代码的优化。
|
||||||
|
|
||||||
所有的系数都被储存在了寄存器中,这意味着访问他们非常快。这里也没有运行时数组访问边界检查。所有这些 Rust 能够提供的优化使得结果代码极为高效。现在知道这些了,请放心大胆的使用迭代器和闭包吧!他们使得代码看起来更高级,但并不为此引入运行时性能损失。
|
所有的系数都被储存在了寄存器中,这意味着访问它们非常快。这里也没有运行时数组访问边界检查。所有这些 Rust 能够提供的优化使得结果代码极为高效。现在知道这些了,请放心大胆的使用迭代器和闭包吧!它们使得代码看起来更高级,但并不为此引入运行时性能损失。
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
闭包和迭代器是 Rust 受函数式编程语言观念所启发的功能。他们对 Rust 以底层的性能来明确的表达高级概念的能力有很大贡献。闭包和迭代器的实现达到了不影响运行时性能的程度。这正是 Rust 竭力提供零成本抽象的目标的一部分。
|
闭包和迭代器是 Rust 受函数式编程语言观念所启发的功能。它们对 Rust 以底层的性能来明确的表达高级概念的能力有很大贡献。闭包和迭代器的实现达到了不影响运行时性能的程度。这正是 Rust 竭力提供零成本抽象的目标的一部分。
|
||||||
|
|
||||||
现在我们改进了我们 I/O 项目的(代码)表现力,让我们看一看更多 `cargo` 的功能,他们将帮助我们准备好将项目分享给世界。
|
现在我们改进了我们 I/O 项目的(代码)表现力,让我们看一看更多 `cargo` 的功能,它们将帮助我们准备好将项目分享给世界。
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 44e31f9f304e0cd9ace01045d17a2aa01a449528
|
> commit 44e31f9f304e0cd9ace01045d17a2aa01a449528
|
||||||
|
|
||||||
在 Rust 中 **发布配置**(*release profiles*)是预定义的、可定制的带有不同选项的配置,他们允许程序员更灵活地控制代码编译的多种选项。每一个配置都彼此相互独立。
|
在 Rust 中 **发布配置**(*release profiles*)是预定义的、可定制的带有不同选项的配置,它们允许程序员更灵活地控制代码编译的多种选项。每一个配置都彼此相互独立。
|
||||||
|
|
||||||
Cargo 有两个主要的配置:运行 `cargo build` 时采用的 `dev` 配置和运行 `cargo build --release` 的 `release` 配置。`dev` 配置被定义为开发时的好的默认配置,`release` 配置则有着良好的发布构建的默认配置。
|
Cargo 有两个主要的配置:运行 `cargo build` 时采用的 `dev` 配置和运行 `cargo build --release` 的 `release` 配置。`dev` 配置被定义为开发时的好的默认配置,`release` 配置则有着良好的发布构建的默认配置。
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ Rust 和 Cargo 有一些帮助他人更方便地找到和使用你发布的包
|
|||||||
|
|
||||||
### 编写有用的文档注释
|
### 编写有用的文档注释
|
||||||
|
|
||||||
准确的包文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用两斜杠 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(_documentation comments_),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的程序员理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。
|
准确的包文档有助于其他用户理解如何以及何时使用它们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用两斜杠 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(_documentation comments_),它们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,它们意在让对库感兴趣的程序员理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。
|
||||||
|
|
||||||
文档注释使用三斜杠 `///` 而不是两斜杆以支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释,
|
文档注释使用三斜杠 `///` 而不是两斜杆以支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释,
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
|
|||||||
|
|
||||||
<span class="caption">示例 14-2:`my_crate` crate 整体的文档</span>
|
<span class="caption">示例 14-2:`my_crate` crate 整体的文档</span>
|
||||||
|
|
||||||
注意 `//!` 的最后一行之后没有任何代码。因为他们以 `//!` 开头而不是 `///`,这是属于包含此注释的项而不是注释之后项的文档。在这个情况下时 _src/lib.rs_ 文件,也就是 crate 根文件。这些注释描述了整个 crate。
|
注意 `//!` 的最后一行之后没有任何代码。因为它们以 `//!` 开头而不是 `///`,这是属于包含此注释的项而不是注释之后项的文档。在这个情况下时 _src/lib.rs_ 文件,也就是 crate 根文件。这些注释描述了整个 crate。
|
||||||
|
|
||||||
如果运行 `cargo doc --open`,将会发现这些注释显示在 `my_crate` 文档的首页,位于 crate 中公有项列表之上,如图 14-2 所示:
|
如果运行 `cargo doc --open`,将会发现这些注释显示在 `my_crate` 文档的首页,位于 crate 中公有项列表之上,如图 14-2 所示:
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
|
|||||||
|
|
||||||
<span class="caption">图 14-2:包含 `my_crate` 整体描述的注释所渲染的文档</span>
|
<span class="caption">图 14-2:包含 `my_crate` 整体描述的注释所渲染的文档</span>
|
||||||
|
|
||||||
位于项之中的文档注释对于描述 crate 和模块特别有用。使用他们描述其容器整体的目的来帮助 crate 用户理解你的代码组织。
|
位于项之中的文档注释对于描述 crate 和模块特别有用。使用它们描述其容器整体的目的来帮助 crate 用户理解你的代码组织。
|
||||||
|
|
||||||
### 使用 `pub use` 导出合适的公有 API
|
### 使用 `pub use` 导出合适的公有 API
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
|
|||||||
|
|
||||||
<span class="caption">图 14-3:包含 `kinds` 和 `utils` 模块的库 `art` 的文档首页</span>
|
<span class="caption">图 14-3:包含 `kinds` 和 `utils` 模块的库 `art` 的文档首页</span>
|
||||||
|
|
||||||
注意 `PrimaryColor` 和 `SecondaryColor` 类型、以及 `mix` 函数都没有在首页中列出。我们必须点击 `kinds` 或 `utils` 才能看到他们。
|
注意 `PrimaryColor` 和 `SecondaryColor` 类型、以及 `mix` 函数都没有在首页中列出。我们必须点击 `kinds` 或 `utils` 才能看到它们。
|
||||||
|
|
||||||
另一个依赖这个库的 crate 需要 `use` 语句来导入 `art` 中的项,这包含指定其当前定义的模块结构。示例 14-4 展示了一个使用 `art` crate 中 `PrimaryColor` 和 `mix` 项的 crate 的例子:
|
另一个依赖这个库的 crate 需要 `use` 语句来导入 `art` 中的项,这包含指定其当前定义的模块结构。示例 14-4 展示了一个使用 `art` crate 中 `PrimaryColor` 和 `mix` 项的 crate 的例子:
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
|
|||||||
|
|
||||||
对于有很多嵌套模块的情况,使用 `pub use` 将类型重导出到顶级结构对于使用 crate 的人来说将会是大为不同的体验。`pub use` 的另一个常见用法是重导出当前 crate 的依赖的定义使其 crate 定义变成你 crate 公有 API 的一部分。
|
对于有很多嵌套模块的情况,使用 `pub use` 将类型重导出到顶级结构对于使用 crate 的人来说将会是大为不同的体验。`pub use` 的另一个常见用法是重导出当前 crate 的依赖的定义使其 crate 定义变成你 crate 公有 API 的一部分。
|
||||||
|
|
||||||
创建一个有用的公有 API 结构更像是一门艺术而非科学,你可以反复检视他们来找出最适合用户的 API。`pub use` 提供了解耦组织 crate 内部结构和与终端用户体现的灵活性。观察一些你所安装的 crate 的代码来看看其内部结构是否不同于公有 API。
|
创建一个有用的公有 API 结构更像是一门艺术而非科学,你可以反复检视它们来找出最适合用户的 API。`pub use` 提供了解耦组织 crate 内部结构和与终端用户体现的灵活性。观察一些你所安装的 crate 的代码来看看其内部结构是否不同于公有 API。
|
||||||
|
|
||||||
### 创建 Crates.io 账号
|
### 创建 Crates.io 账号
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ license = "MIT OR Apache-2.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
```
|
```
|
||||||
|
|
||||||
[Cargo 的文档](http://doc.rust-lang.org/cargo/) 描述了其他可以指定的元信息,他们可以帮助你的 crate 更容易被发现和使用!
|
[Cargo 的文档](http://doc.rust-lang.org/cargo/) 描述了其他可以指定的元信息,它们可以帮助你的 crate 更容易被发现和使用!
|
||||||
|
|
||||||
### 发布到 Crates.io
|
### 发布到 Crates.io
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ $ cargo publish
|
|||||||
|
|
||||||
### 使用 `cargo yank` 从 Crates.io 弃用版本
|
### 使用 `cargo yank` 从 Crates.io 弃用版本
|
||||||
|
|
||||||
虽然你不能删除之前版本的 crate,但是可以阻止任何将来的项目将他们加入到依赖中。这在某个版本因为这样或那样的原因被破坏的情况很有用。对于这种情况,Cargo 支持 **撤回**(_yanking_)某个版本。
|
虽然你不能删除之前版本的 crate,但是可以阻止任何将来的项目将它们加入到依赖中。这在某个版本因为这样或那样的原因被破坏的情况很有用。对于这种情况,Cargo 支持 **撤回**(_yanking_)某个版本。
|
||||||
|
|
||||||
撤回某个版本会阻止新项目依赖此版本,不过所有现存此依赖的项目仍然能够下载和依赖这个版本。从本质上说,撤回意味着所有带有 _Cargo.lock_ 的项目的依赖不会被破坏,同时任何新生成的 _Cargo.lock_ 将不能使用被撤回的版本。
|
撤回某个版本会阻止新项目依赖此版本,不过所有现存此依赖的项目仍然能够下载和依赖这个版本。从本质上说,撤回意味着所有带有 _Cargo.lock_ 的项目的依赖不会被破坏,同时任何新生成的 _Cargo.lock_ 将不能使用被撤回的版本。
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ warning: `add_one` (lib) generated 1 warning
|
|||||||
Finished dev [unoptimized + debuginfo] target(s) in 10.18s
|
Finished dev [unoptimized + debuginfo] target(s) in 10.18s
|
||||||
```
|
```
|
||||||
|
|
||||||
现在顶级的 *Cargo.lock* 包含了 `add_one` 的 `rand` 依赖的信息。然而,即使 `rand` 被用于工作空间的某处,也不能在其他 crate 中使用它,除非也在他们的 *Cargo.toml* 中加入 `rand`。例如,如果在顶级的 `adder` crate 的 *adder/src/main.rs* 中增加 `use rand;`,会得到一个错误:
|
现在顶级的 *Cargo.lock* 包含了 `add_one` 的 `rand` 依赖的信息。然而,即使 `rand` 被用于工作空间的某处,也不能在其他 crate 中使用它,除非也在它们的 *Cargo.toml* 中加入 `rand`。例如,如果在顶级的 `adder` crate 的 *adder/src/main.rs* 中增加 `use rand;`,会得到一个错误:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ cargo build
|
$ cargo build
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit c084bdd9ee328e7e774df19882ccc139532e53d8
|
> commit c084bdd9ee328e7e774df19882ccc139532e53d8
|
||||||
|
|
||||||
Cargo 的设计使得开发者可以通过新的子命令来对 Cargo 进行扩展,而无需修改 Cargo 本身。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是 Cargo 设计上的一个非常方便的优点!
|
Cargo 的设计使得开发者可以通过新的子命令来对 Cargo 进行扩展,而无需修改 Cargo 本身。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行它们是 Cargo 设计上的一个非常方便的优点!
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 5a3a64d60b0dd786c35ca4daada7a4d20da33e5e
|
> commit 5a3a64d60b0dd786c35ca4daada7a4d20da33e5e
|
||||||
|
|
||||||
**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。
|
**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。
|
||||||
|
|
||||||
另一方面,**智能指针**(*smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了多于引用的额外功能。为了探索其基本概念,我们来看看一些智能指针的例子,这包括 **引用计数** (*reference counting*)智能指针类型。这种指针允许数据有多个所有者,它会记录所有者的数量,当没有所有者时清理数据。在 Rust 中因为引用和借用,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 **拥有** 他们指向的数据。
|
另一方面,**智能指针**(*smart pointers*)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了多于引用的额外功能。为了探索其基本概念,我们来看看一些智能指针的例子,这包括 **引用计数** (*reference counting*)智能指针类型。这种指针允许数据有多个所有者,它会记录所有者的数量,当没有所有者时清理数据。在 Rust 中因为引用和借用,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 **拥有** 它们指向的数据。
|
||||||
|
|
||||||
虽然当时没有这么称呼它们,实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec<T>`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也拥有元数据和额外的功能或保证。例如 `String` 存储了其容量作为元数据,并拥有额外的能力确保其数据总是有效的 UTF-8 编码。
|
虽然当时没有这么称呼它们,实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec<T>`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也拥有元数据和额外的功能或保证。例如 `String` 存储了其容量作为元数据,并拥有额外的能力确保其数据总是有效的 UTF-8 编码。
|
||||||
|
|
||||||
智能指针通常使用结构体实现。智能指针不同于结构体的地方在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。
|
智能指针通常使用结构体实现。智能指针不同于结构体的地方在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说它们很重要。
|
||||||
|
|
||||||
考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些:
|
考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些:
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ _cons list_ 是一个来源于 Lisp 编程语言及其方言的数据结构,
|
|||||||
(1, (2, (3, Nil)))
|
(1, (2, (3, Nil)))
|
||||||
```
|
```
|
||||||
|
|
||||||
cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。
|
cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,它们代表无效或缺失的值。
|
||||||
|
|
||||||
cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec<T>` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。
|
cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec<T>` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` repre
|
|||||||
|
|
||||||
<span class="caption">图 15-2:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了</span>
|
<span class="caption">图 15-2:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了</span>
|
||||||
|
|
||||||
box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。
|
box 只提供了间接存储和堆分配;它们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以它们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。
|
||||||
|
|
||||||
`Box<T>` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box<T>` 值被当作引用对待。当 `Box<T>` 值离开作用域时,由于 `Box<T>` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。这两个 trait 对于在本章余下讨论的其他智能指针所提供的功能中,将会更为重要。让我们更详细的探索一下这两个 trait。
|
`Box<T>` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box<T>` 值被当作引用对待。当 `Box<T>` 值离开作用域时,由于 `Box<T>` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。这两个 trait 对于在本章余下讨论的其他智能指针所提供的功能中,将会更为重要。让我们更详细的探索一下这两个 trait。
|
||||||
|
|
||||||
|
@ -155,11 +155,11 @@
|
|||||||
|
|
||||||
这里创建了一个 `Rc<RefCell<i32>>` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 成员创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。
|
这里创建了一个 `Rc<RefCell<i32>>` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 成员创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。
|
||||||
|
|
||||||
我们将列表 `a` 封装进了 `Rc<T>` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-18 一样。
|
我们将列表 `a` 封装进了 `Rc<T>` 这样当创建列表 `b` 和 `c` 时,它们都可以引用 `a`,正如示例 15-18 一样。
|
||||||
|
|
||||||
一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自动解引用功能([“`->` 运算符到哪去了?”][wheres-the---operator] 部分)来解引用 `Rc<T>` 以获取其内部的 `RefCell<T>` 值。`borrow_mut` 方法返回 `RefMut<T>` 智能指针,可以对其使用解引用运算符并修改其内部值。
|
一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自动解引用功能([“`->` 运算符到哪去了?”][wheres-the---operator] 部分)来解引用 `Rc<T>` 以获取其内部的 `RefCell<T>` 值。`borrow_mut` 方法返回 `RefMut<T>` 智能指针,可以对其使用解引用运算符并修改其内部值。
|
||||||
|
|
||||||
当我们打印出 `a`、`b` 和 `c` 时,可以看到他们都拥有修改后的值 15 而不是 5:
|
当我们打印出 `a`、`b` 和 `c` 时,可以看到它们都拥有修改后的值 15 而不是 5:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
{{#include ../listings/ch15-smart-pointers/listing-15-24/output.txt}}
|
{{#include ../listings/ch15-smart-pointers/listing-15-24/output.txt}}
|
||||||
|
@ -56,11 +56,11 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理
|
|||||||
|
|
||||||
### 避免引用循环:将 `Rc<T>` 变为 `Weak<T>`
|
### 避免引用循环:将 `Rc<T>` 变为 `Weak<T>`
|
||||||
|
|
||||||
到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc<T>` 实例的 `strong_count`,和只在其 `strong_count` 为 0 时才会被清理的 `Rc<T>` 实例。你也可以通过调用 `Rc::downgrade` 并传递 `Rc<T>` 实例的引用来创建其值的 **弱引用**(_weak reference_)。强引用代表如何共享 `Rc<T>` 实例的所有权。弱引用并不属于所有权关系,当 `Rc<T>` 实例被清理时其计数没有影响。他们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。
|
到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc<T>` 实例的 `strong_count`,和只在其 `strong_count` 为 0 时才会被清理的 `Rc<T>` 实例。你也可以通过调用 `Rc::downgrade` 并传递 `Rc<T>` 实例的引用来创建其值的 **弱引用**(_weak reference_)。强引用代表如何共享 `Rc<T>` 实例的所有权。弱引用并不属于所有权关系,当 `Rc<T>` 实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。
|
||||||
|
|
||||||
调用 `Rc::downgrade` 时会得到 `Weak<T>` 类型的智能指针。不同于将 `Rc<T>` 实例的 `strong_count` 加 1,调用 `Rc::downgrade` 会将 `weak_count` 加 1。`Rc<T>` 类型使用 `weak_count` 来记录其存在多少个 `Weak<T>` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc<T>` 实例被清理。
|
调用 `Rc::downgrade` 时会得到 `Weak<T>` 类型的智能指针。不同于将 `Rc<T>` 实例的 `strong_count` 加 1,调用 `Rc::downgrade` 会将 `weak_count` 加 1。`Rc<T>` 类型使用 `weak_count` 来记录其存在多少个 `Weak<T>` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc<T>` 实例被清理。
|
||||||
|
|
||||||
强引用代表如何共享 `Rc<T>` 实例的所有权,但弱引用并不属于所有权关系。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
|
强引用代表如何共享 `Rc<T>` 实例的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
|
||||||
|
|
||||||
因为 `Weak<T>` 引用的值可能已经被丢弃了,为了使用 `Weak<T>` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak<T>` 实例的 `upgrade` 方法,这会返回 `Option<Rc<T>>`。如果 `Rc<T>` 值还未被丢弃,则结果是 `Some`;如果 `Rc<T>` 已被丢弃,则结果是 `None`。因为 `upgrade` 返回一个 `Option<Rc<T>>`,Rust 会确保处理 `Some` 和 `None` 的情况,所以它不会返回非法指针。
|
因为 `Weak<T>` 引用的值可能已经被丢弃了,为了使用 `Weak<T>` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak<T>` 实例的 `upgrade` 方法,这会返回 `Option<Rc<T>>`。如果 `Rc<T>` 值还未被丢弃,则结果是 `Some`;如果 `Rc<T>` 已被丢弃,则结果是 `None`。因为 `upgrade` 返回一个 `Option<Rc<T>>`,Rust 会确保处理 `Some` 和 `None` 的情况,所以它不会返回非法指针。
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理
|
|||||||
|
|
||||||
<span class="caption">示例 15-27:创建没有子节点的 `leaf` 节点和以 `leaf` 作为子节点的 `branch` 节点</span>
|
<span class="caption">示例 15-27:创建没有子节点的 `leaf` 节点和以 `leaf` 作为子节点的 `branch` 节点</span>
|
||||||
|
|
||||||
这里克隆了 `leaf` 中的 `Rc<Node>` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children` 从 `branch` 中获得 `leaf`,不过无法从 `leaf` 到 `branch`。`leaf` 没有到 `branch` 的引用且并不知道他们相互关联。我们希望 `leaf` 知道 `branch` 是其父节点。稍后我们会这么做。
|
这里克隆了 `leaf` 中的 `Rc<Node>` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children` 从 `branch` 中获得 `leaf`,不过无法从 `leaf` 到 `branch`。`leaf` 没有到 `branch` 的引用且并不知道它们相互关联。我们希望 `leaf` 知道 `branch` 是其父节点。稍后我们会这么做。
|
||||||
|
|
||||||
#### 增加从子到父的引用
|
#### 增加从子到父的引用
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ Got: hi
|
|||||||
|
|
||||||
<span class="caption">示例 16-10: 发送多个消息,并在每次发送后暂停一段时间</span>
|
<span class="caption">示例 16-10: 发送多个消息,并在每次发送后暂停一段时间</span>
|
||||||
|
|
||||||
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。
|
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历它们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。
|
||||||
|
|
||||||
在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。
|
在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。
|
||||||
|
|
||||||
@ -138,6 +138,6 @@ Got: thread
|
|||||||
Got: you
|
Got: you
|
||||||
```
|
```
|
||||||
|
|
||||||
虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 `thread::sleep` 做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定,且每次都会产生不同的输出。
|
虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 `thread::sleep` 做实验,在不同的线程中提供不同的值,就会发现它们的运行更加不确定,且每次都会产生不同的输出。
|
||||||
|
|
||||||
现在我们见识过了信道如何工作,再看看另一种不同的并发方式吧。
|
现在我们见识过了信道如何工作,再看看另一种不同的并发方式吧。
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 16-13: 程序启动了 10 个线程,每个线程都通过 `Mutex<T>` 来增加计数器的值</span>
|
<span class="caption">示例 16-13: 程序启动了 10 个线程,每个线程都通过 `Mutex<T>` 来增加计数器的值</span>
|
||||||
|
|
||||||
这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex<T>`,类似示例 16-12 那样。接下来遍历 range 创建了 10 个线程。使用了 `thread::spawn` 并对所有线程使用了相同的闭包:他们每一个都将调用 `lock` 方法来获取 `Mutex<T>` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
|
这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex<T>`,类似示例 16-12 那样。接下来遍历 range 创建了 10 个线程。使用了 `thread::spawn` 并对所有线程使用了相同的闭包:它们每一个都将调用 `lock` 方法来获取 `Mutex<T>` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
|
||||||
|
|
||||||
在主线程中,我们像示例 16-2 那样收集了所有的 join 句柄,调用它们的 `join` 方法来确保所有线程都会结束。这时,主线程会获取锁并打印出程序的结果。
|
在主线程中,我们像示例 16-2 那样收集了所有的 join 句柄,调用它们的 `join` 方法来确保所有线程都会结束。这时,主线程会获取锁并打印出程序的结果。
|
||||||
|
|
||||||
@ -123,8 +123,8 @@ Result: 10
|
|||||||
|
|
||||||
你可能注意到了,因为 `counter` 是不可变的,不过可以获取其内部值的可变引用;这意味着 `Mutex<T>` 提供了内部可变性,就像 `Cell` 系列类型那样。正如第十五章中使用 `RefCell<T>` 可以改变 `Rc<T>` 中的内容那样,同样的可以使用 `Mutex<T>` 来改变 `Arc<T>` 中的内容。
|
你可能注意到了,因为 `counter` 是不可变的,不过可以获取其内部值的可变引用;这意味着 `Mutex<T>` 提供了内部可变性,就像 `Cell` 系列类型那样。正如第十五章中使用 `RefCell<T>` 可以改变 `Rc<T>` 中的内容那样,同样的可以使用 `Mutex<T>` 来改变 `Arc<T>` 中的内容。
|
||||||
|
|
||||||
另一个值得注意的细节是 Rust 不能避免使用 `Mutex<T>` 的全部逻辑错误。回忆一下第十五章使用 `Rc<T>` 就有造成引用循环的风险,这时两个 `Rc<T>` 值相互引用,造成内存泄漏。同理,`Mutex<T>` 也有造成 **死锁**(_deadlock_)的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中 `Mutex<T>` 和 `MutexGuard` 的 API 文档会提供有用的信息。
|
另一个值得注意的细节是 Rust 不能避免使用 `Mutex<T>` 的全部逻辑错误。回忆一下第十五章使用 `Rc<T>` 就有造成引用循环的风险,这时两个 `Rc<T>` 值相互引用,造成内存泄漏。同理,`Mutex<T>` 也有造成 **死锁**(_deadlock_)的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现它们。标准库中 `Mutex<T>` 和 `MutexGuard` 的 API 文档会提供有用的信息。
|
||||||
|
|
||||||
接下来,为了丰富本章的内容,让我们讨论一下 `Send`和 `Sync` trait 以及如何对自定义类型使用他们。
|
接下来,为了丰富本章的内容,让我们讨论一下 `Send`和 `Sync` trait 以及如何对自定义类型使用它们。
|
||||||
|
|
||||||
[atomic]: https://doc.rust-lang.org/std/sync/atomic/index.html
|
[atomic]: https://doc.rust-lang.org/std/sync/atomic/index.html
|
||||||
|
@ -24,15 +24,15 @@ Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 **
|
|||||||
|
|
||||||
### 手动实现 `Send` 和 `Sync` 是不安全的
|
### 手动实现 `Send` 和 `Sync` 是不安全的
|
||||||
|
|
||||||
通常并不需要手动实现 `Send` 和 `Sync` trait,因为由 `Send` 和 `Sync` 的类型组成的类型,自动就是 `Send` 和 `Sync` 的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。
|
通常并不需要手动实现 `Send` 和 `Sync` trait,因为由 `Send` 和 `Sync` 的类型组成的类型,自动就是 `Send` 和 `Sync` 的。因为它们是标记 trait,甚至都不需要实现任何方法。们只是用来加强并发相关的不可变性的。
|
||||||
|
|
||||||
手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send` 和 `Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[“The Rustonomicon”][nomicon] 中有更多关于这些保证以及如何维持他们的信息。
|
手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send` 和 `Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[“The Rustonomicon”][nomicon] 中有更多关于这些保证以及如何维持它们的信息。
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
这不会是本书最后一个出现并发的章节:第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
|
这不会是本书最后一个出现并发的章节:第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
|
||||||
|
|
||||||
正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
|
正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。它们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
|
||||||
|
|
||||||
Rust 提供了用于消息传递的信道,和像 `Mutex<T>` 和 `Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧!
|
Rust 提供了用于消息传递的信道,和像 `Mutex<T>` 和 `Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧!
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
> <br>
|
> <br>
|
||||||
> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892
|
> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892
|
||||||
|
|
||||||
关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第十三章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。
|
关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第十三章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持它们。
|
||||||
|
|
||||||
### 对象包含数据和行为
|
### 对象包含数据和行为
|
||||||
|
|
||||||
@ -16,7 +16,7 @@
|
|||||||
>
|
>
|
||||||
> 面向对象的程序是由对象组成的。一个 **对象** 包含数据和操作这些数据的过程。这些过程通常被称为 **方法** 或 **操作**。
|
> 面向对象的程序是由对象组成的。一个 **对象** 包含数据和操作这些数据的过程。这些过程通常被称为 **方法** 或 **操作**。
|
||||||
|
|
||||||
在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 `impl` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是他们提供了与对象相同的功能,参考 *The Gang of Four* 中对象的定义。
|
在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 `impl` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是它们提供了与对象相同的功能,参考 *The Gang of Four* 中对象的定义。
|
||||||
|
|
||||||
### 封装隐藏了实现细节
|
### 封装隐藏了实现细节
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
这里调用 `Option` 的 `as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option<Box<dyn State>>`,调用 `as_ref` 会返回一个 `Option<&Box<dyn State>>`。如果不调用 `as_ref`,将会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。
|
这里调用 `Option` 的 `as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option<Box<dyn State>>`,调用 `as_ref` 会返回一个 `Option<&Box<dyn State>>`。如果不调用 `as_ref`,将会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。
|
||||||
|
|
||||||
接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 [“当我们比编译器知道更多的情况”][more-info-than-rustc] 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。
|
接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在它们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 [“当我们比编译器知道更多的情况”][more-info-than-rustc] 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。
|
||||||
|
|
||||||
接着我们就有了一个 `&Box<dyn State>`,当调用其 `content` 时,Deref 强制转换会作用于 `&` 和 `Box` ,这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示:
|
接着我们就有了一个 `&Box<dyn State>`,当调用其 `content` 时,Deref 强制转换会作用于 `&` 和 `Box` ,这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示:
|
||||||
|
|
||||||
@ -177,9 +177,9 @@
|
|||||||
|
|
||||||
状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `PendingReview` 和 `Published` 之间增加另一个状态,比如 `Scheduled`,则不得不修改 `PendingReview` 中的代码来转移到 `Scheduled`。如果 `PendingReview` 无需因为新增的状态而改变就更好了,不过这意味着切换到另一种设计模式。
|
状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `PendingReview` 和 `Published` 之间增加另一个状态,比如 `Scheduled`,则不得不修改 `PendingReview` 中的代码来转移到 `Scheduled`。如果 `PendingReview` 无需因为新增的状态而改变就更好了,不过这意味着切换到另一种设计模式。
|
||||||
|
|
||||||
另一个缺点是我们会发现一些重复的逻辑。为了消除他们,可以尝试为 `State` trait 中返回 `self` 的 `request_review` 和 `approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要其方法是对象安全的。
|
另一个缺点是我们会发现一些重复的逻辑。为了消除它们,可以尝试为 `State` trait 中返回 `self` 的 `request_review` 和 `approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要其方法是对象安全的。
|
||||||
|
|
||||||
另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第十九章的 [“宏”][macros] 部分)。
|
另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。它们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第十九章的 [“宏”][macros] 部分)。
|
||||||
|
|
||||||
完全按照面向对象语言的定义实现这个模式并没有尽可能地利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。
|
完全按照面向对象语言的定义实现这个模式并没有尽可能地利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。
|
||||||
|
|
||||||
@ -225,7 +225,7 @@
|
|||||||
|
|
||||||
`request_review` 和 `approve` 方法获取 `self` 的所有权,因此会消费 `DraftPost` 和 `PendingReviewPost` 实例,并分别转换为 `PendingReviewPost` 和发布的 `Post`。这样在调用 `request_review` 之后就不会遗留任何 `DraftPost` 实例,后者同理。`PendingReviewPost` 并没有定义 `content` 方法,所以尝试读取其内容会导致编译错误,`DraftPost` 同理。因为唯一得到定义了 `content` 方法的 `Post` 实例的途径是调用 `PendingReviewPost` 的 `approve` 方法,而得到 `PendingReviewPost` 的唯一办法是调用 `DraftPost` 的 `request_review` 方法,现在我们就将发博文的工作流编码进了类型系统。
|
`request_review` 和 `approve` 方法获取 `self` 的所有权,因此会消费 `DraftPost` 和 `PendingReviewPost` 实例,并分别转换为 `PendingReviewPost` 和发布的 `Post`。这样在调用 `request_review` 之后就不会遗留任何 `DraftPost` 实例,后者同理。`PendingReviewPost` 并没有定义 `content` 方法,所以尝试读取其内容会导致编译错误,`DraftPost` 同理。因为唯一得到定义了 `content` 方法的 `Post` 实例的途径是调用 `PendingReviewPost` 的 `approve` 方法,而得到 `PendingReviewPost` 的唯一办法是调用 `DraftPost` 的 `request_review` 方法,现在我们就将发博文的工作流编码进了类型系统。
|
||||||
|
|
||||||
这也意味着不得不对 `main` 做出一些小的修改。因为 `request_review` 和 `approve` 返回新实例而不是修改被调用的结构体,所以我们需要增加更多的 `let post = ` 覆盖赋值来保存返回的实例。也不再能断言草案和等待审核的博文的内容为空字符串了,我们也不再需要他们:不能编译尝试使用这些状态下博文内容的代码。更新后的 `main` 的代码如示例 17-21 所示:
|
这也意味着不得不对 `main` 做出一些小的修改。因为 `request_review` 和 `approve` 返回新实例而不是修改被调用的结构体,所以我们需要增加更多的 `let post = ` 覆盖赋值来保存返回的实例。也不再能断言草案和等待审核的博文的内容为空字符串了,我们也不再需要它们:不能编译尝试使用这些状态下博文内容的代码。更新后的 `main` 的代码如示例 17-21 所示:
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ match x {
|
|||||||
{{#include ../listings/ch18-patterns-and-matching/listing-18-03/output.txt}}
|
{{#include ../listings/ch18-patterns-and-matching/listing-18-03/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里使用 `enumerate` 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。第一个产生的值是元组 `(0, 'a')`。当这个值匹配模式 `(index, value)`,`index` 将会是 0 而 `value` 将会是 `'a'`,并打印出第一行输出。
|
这里使用 `enumerate` 方法适配一个迭代器来产生一个值和其在迭代器中的索引,它们位于一个元组中。第一个产生的值是元组 `(0, 'a')`。当这个值匹配模式 `(index, value)`,`index` 将会是 0 而 `value` 将会是 `'a'`,并打印出第一行输出。
|
||||||
|
|
||||||
### `let` 语句
|
### `let` 语句
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ let PATTERN = EXPRESSION;
|
|||||||
|
|
||||||
因为如第十三章所讲闭包类似于函数,也可以在闭包参数列表中使用模式。
|
因为如第十三章所讲闭包类似于函数,也可以在闭包参数列表中使用模式。
|
||||||
|
|
||||||
现在我们见过了很多使用模式的方式了,不过模式在每个使用它的地方并不以相同的方式工作;在一些地方,模式必须是 *irrefutable* 的,意味着他们必须匹配所提供的任何值。在另一些情况,他们则可以是 refutable 的。接下来让我们讨论这两个概念。
|
现在我们见过了很多使用模式的方式了,不过模式在每个使用它的地方并不以相同的方式工作;在一些地方,模式必须是 *irrefutable* 的,意味着它们必须匹配所提供的任何值。在另一些情况,它们则可以是 refutable 的。接下来让我们讨论这两个概念。
|
||||||
|
|
||||||
[ignoring-values-in-a-pattern]:
|
[ignoring-values-in-a-pattern]:
|
||||||
ch18-03-pattern-syntax.html#忽略模式中的值
|
ch18-03-pattern-syntax.html#忽略模式中的值
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
注意这里没有引入 `unsafe` 关键字。可以在安全代码中 **创建** 裸指针,只是不能在不安全块之外 **解引用** 裸指针,稍后便会看到。
|
注意这里没有引入 `unsafe` 关键字。可以在安全代码中 **创建** 裸指针,只是不能在不安全块之外 **解引用** 裸指针,稍后便会看到。
|
||||||
|
|
||||||
这里使用 `as` 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。
|
这里使用 `as` 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建它们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。
|
||||||
|
|
||||||
作为展示接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。通常没有好的理由编写这样的代码,不过却是可行的:
|
作为展示接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。通常没有好的理由编写这样的代码,不过却是可行的:
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同
|
|||||||
|
|
||||||
### 访问或修改可变静态变量
|
### 访问或修改可变静态变量
|
||||||
|
|
||||||
目前为止全书都尽量避免讨论 **全局变量**(*global variables*),Rust 确实支持他们,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
|
目前为止全书都尽量避免讨论 **全局变量**(*global variables*),Rust 确实支持它们,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
|
||||||
|
|
||||||
全局变量在 Rust 中被称为 **静态**(*static*)变量。示例 19-9 展示了一个拥有字符串 slice 值的静态变量的声明和应用:
|
全局变量在 Rust 中被称为 **静态**(*static*)变量。示例 19-9 展示了一个拥有字符串 slice 值的静态变量的声明和应用:
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同
|
|||||||
|
|
||||||
通过 `unsafe impl`,我们承诺将保证编译器所不能验证的不变量。
|
通过 `unsafe impl`,我们承诺将保证编译器所不能验证的不变量。
|
||||||
|
|
||||||
作为一个例子,回忆第十六章 [“使用 `Sync` 和 `Send` trait 的可扩展并发”][extensible-concurrency-with-the-sync-and-send-traits] 部分中的 `Sync` 和 `Send` 标记 trait,编译器会自动为完全由 `Send` 和 `Sync` 类型组成的类型自动实现他们。如果实现了一个包含一些不是 `Send` 或 `Sync` 的类型,比如裸指针,并希望将此类型标记为 `Send` 或 `Sync`,则必须使用 `unsafe`。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 `unsafe` 表明。
|
作为一个例子,回忆第十六章 [“使用 `Sync` 和 `Send` trait 的可扩展并发”][extensible-concurrency-with-the-sync-and-send-traits] 部分中的 `Sync` 和 `Send` 标记 trait,编译器会自动为完全由 `Send` 和 `Sync` 类型组成的类型自动实现它们。如果实现了一个包含一些不是 `Send` 或 `Sync` 的类型,比如裸指针,并希望将此类型标记为 `Send` 或 `Sync`,则必须使用 `unsafe`。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 `unsafe` 表明。
|
||||||
|
|
||||||
### 访问联合体中的字段
|
### 访问联合体中的字段
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ Rust 提供了声明 **类型别名**(*type alias*)的能力,使用 `type`
|
|||||||
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-04-kilometers-alias/src/main.rs:there}}
|
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-04-kilometers-alias/src/main.rs:there}}
|
||||||
```
|
```
|
||||||
|
|
||||||
因为 `Kilometers` 是 `i32` 的别名,他们是同一类型,可以将 `i32` 与 `Kilometers` 相加,也可以将 `Kilometers` 传递给获取 `i32` 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。换句话说,如果在哪里混用 `Kilometers` 和 `i32` 的值,编译器也不会给出一个错误。
|
因为 `Kilometers` 是 `i32` 的别名,它们是同一类型,可以将 `i32` 与 `Kilometers` 相加,也可以将 `Kilometers` 传递给获取 `i32` 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。换句话说,如果在哪里混用 `Kilometers` 和 `i32` 的值,编译器也不会给出一个错误。
|
||||||
|
|
||||||
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
|
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ never type 的另一个用途是 `panic!`。还记得 `Option<T>` 上的 `unwrap
|
|||||||
|
|
||||||
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 **动态大小类型**(*dynamically sized types*)。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。
|
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 **动态大小类型**(*dynamically sized types*)。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。
|
||||||
|
|
||||||
让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`。没错,不是 `&str`,而是 `str` 本身。`str` 是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,他们不能工作:
|
让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`。没错,不是 `&str`,而是 `str` 本身。`str` 是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,它们不能工作:
|
||||||
|
|
||||||
```rust,ignore,does_not_compile
|
```rust,ignore,does_not_compile
|
||||||
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-11-cant-create-str/src/main.rs:here}}
|
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-11-cant-create-str/src/main.rs:here}}
|
||||||
@ -134,9 +134,9 @@ Rust 需要知道有关类型的某些细节,例如为特定类型的值需要
|
|||||||
|
|
||||||
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
|
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
|
||||||
|
|
||||||
那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 部分,slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是 **两个** 值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:他们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
|
那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 部分,slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是 **两个** 值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
|
||||||
|
|
||||||
可以将 `str` 与所有类型的指针结合:比如 `Box<str>` 或 `Rc<str>`。事实上,之前我们已经见过了,不过是另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十七章 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 `&dyn Trait` 或 `Box<dyn Trait>`(`Rc<dyn Trait>` 也可以)。
|
可以将 `str` 与所有类型的指针结合:比如 `Box<str>` 或 `Rc<str>`。事实上,之前我们已经见过了,不过是另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十七章 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分,我们提到了为了将 trait 用于 trait 对象,必须将它们放入指针之后,比如 `&dyn Trait` 或 `Box<dyn Trait>`(`Rc<dyn Trait>` 也可以)。
|
||||||
|
|
||||||
为了处理 DST,Rust 提供了 `Sized` trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说,对于如下泛型函数定义:
|
为了处理 DST,Rust 提供了 `Sized` trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说,对于如下泛型函数定义:
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
### 返回闭包
|
### 返回闭包
|
||||||
|
|
||||||
闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为他们没有一个可返回的具体类型;例如不允许使用函数指针 `fn` 作为返回值类型。
|
闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;例如不允许使用函数指针 `fn` 作为返回值类型。
|
||||||
|
|
||||||
这段代码尝试直接返回闭包,它并不能编译:
|
这段代码尝试直接返回闭包,它并不能编译:
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ pub fn some_name(input: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
### 如何编写自定义 `derive` 宏
|
### 如何编写自定义 `derive` 宏
|
||||||
|
|
||||||
让我们创建一个 `hello_macro` crate,其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让用户为其每一个类型实现 `HelloMacro` trait,我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate,使程序员能够写类似示例 19-30 中的代码。
|
让我们创建一个 `hello_macro` crate,其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让用户为其每一个类型实现 `HelloMacro` trait,我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解它们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate,使程序员能够写类似示例 19-30 中的代码。
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ $ cargo new hello_macro_derive --lib
|
|||||||
|
|
||||||
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但现在我们组织项目的方式使编程人员在无需 `derive` 功能时也能够单独使用 `hello_macro`。
|
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但现在我们组织项目的方式使编程人员在无需 `derive` 功能时也能够单独使用 `hello_macro`。
|
||||||
|
|
||||||
我们需要声明 `hello_macro_derive` crate 是过程宏 (proc-macro) crate。我们还需要 `syn` 和 `quote` crate 中的功能,正如你即将看到的,需要将他们加到依赖中。将下面的代码加入到 `hello_macro_derive` 的 *Cargo.toml* 文件中。
|
我们需要声明 `hello_macro_derive` crate 是过程宏 (proc-macro) crate。我们还需要 `syn` 和 `quote` crate 中的功能,正如你即将看到的,需要将它们加到依赖中。将下面的代码加入到 `hello_macro_derive` 的 *Cargo.toml* 文件中。
|
||||||
|
|
||||||
<span class="filename">文件名:hello_macro_derive/Cargo.toml</span>
|
<span class="filename">文件名:hello_macro_derive/Cargo.toml</span>
|
||||||
|
|
||||||
@ -270,7 +270,7 @@ pub fn sql(input: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。
|
好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到它们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。
|
||||||
|
|
||||||
接下来,我们将再开始一个项目,将本书所学的所有内容付与实践!
|
接下来,我们将再开始一个项目,将本书所学的所有内容付与实践!
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
不同于分配无限的线程,线程池中将有固定数量的等待线程。当新进请求时,将请求发送到线程池中做处理。线程池会维护一个接收请求的队列。每一个线程会从队列中取出一个请求,处理请求,接着向队列索取另一个请求。通过这种设计,则可以并发处理 `N` 个请求,其中 `N` 为线程数。如果每一个线程都在响应慢请求,之后的请求仍然会阻塞队列,不过相比之前增加了能处理的慢请求的数量。
|
不同于分配无限的线程,线程池中将有固定数量的等待线程。当新进请求时,将请求发送到线程池中做处理。线程池会维护一个接收请求的队列。每一个线程会从队列中取出一个请求,处理请求,接着向队列索取另一个请求。通过这种设计,则可以并发处理 `N` 个请求,其中 `N` 为线程数。如果每一个线程都在响应慢请求,之后的请求仍然会阻塞队列,不过相比之前增加了能处理的慢请求的数量。
|
||||||
|
|
||||||
这个设计仅仅是多种改善 web server 吞吐量的方法之一。其他可供探索的方法有 **fork/join 模型**(*fork/join model*)、**单线程异步 I/O 模型**(*single-threaded async I/O model*)或者**多线程异步 I/O 模型**(*multi-threaded async I/O model*)。如果你对这个主题感兴趣,则可以阅读更多关于其他解决方案的内容并尝试实现他们;对于一个像 Rust 这样的底层语言,所有这些方法都是可能的。
|
这个设计仅仅是多种改善 web server 吞吐量的方法之一。其他可供探索的方法有 **fork/join 模型**(*fork/join model*)、**单线程异步 I/O 模型**(*single-threaded async I/O model*)或者**多线程异步 I/O 模型**(*multi-threaded async I/O model*)。如果你对这个主题感兴趣,则可以阅读更多关于其他解决方案的内容并尝试实现它们;对于一个像 Rust 这样的底层语言,所有这些方法都是可能的。
|
||||||
|
|
||||||
在开始之前,让我们讨论一下线程池应用看起来怎样。当尝试设计代码时,首先编写客户端接口确实有助于指导代码设计。以期望的调用方式来构建 API 代码的结构,接着在这个结构之内实现功能,而不是先实现功能再设计公有 API。
|
在开始之前,让我们讨论一下线程池应用看起来怎样。当尝试设计代码时,首先编写客户端接口确实有助于指导代码设计。以期望的调用方式来构建 API 代码的结构,接着在这个结构之内实现功能,而不是先实现功能再设计公有 API。
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
|
|||||||
|
|
||||||
#### 分配空间以储存线程
|
#### 分配空间以储存线程
|
||||||
|
|
||||||
现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回结构体之前将他们储存在 `ThreadPool` 结构体中。不过如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名:
|
现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回结构体之前将它们储存在 `ThreadPool` 结构体中。不过如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
|
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
|
||||||
@ -242,7 +242,7 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
|
|||||||
|
|
||||||
下一个需要解决的问题是传递给 `thread::spawn` 的闭包完全没有做任何工作。目前,我们在 `execute` 方法中获得期望执行的闭包,不过在创建 `ThreadPool` 的过程中创建每一个 `Worker` 时需要向 `thread::spawn` 传递一个闭包。
|
下一个需要解决的问题是传递给 `thread::spawn` 的闭包完全没有做任何工作。目前,我们在 `execute` 方法中获得期望执行的闭包,不过在创建 `ThreadPool` 的过程中创建每一个 `Worker` 时需要向 `thread::spawn` 传递一个闭包。
|
||||||
|
|
||||||
我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取需要执行的代码,并发送到线程中执行他们。
|
我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取需要执行的代码,并发送到线程中执行它们。
|
||||||
|
|
||||||
在第十六章,我们学习了 **信道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里信道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划:
|
在第十六章,我们学习了 **信道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里信道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划:
|
||||||
|
|
||||||
@ -264,7 +264,7 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
|
|||||||
|
|
||||||
在 `ThreadPool::new` 中,新建了一个信道,并接着让线程池在接收端等待。这段代码能够成功编译。
|
在 `ThreadPool::new` 中,新建了一个信道,并接着让线程池在接收端等待。这段代码能够成功编译。
|
||||||
|
|
||||||
让我们尝试在线程池创建每个 worker 时将接收者传递给他们。须知我们希望在 worker 所分配的线程中使用接收者,所以将在闭包中引用 `receiver` 参数。示例 20-17 中展示的代码还不能编译:
|
让我们尝试在线程池创建每个 worker 时将接收者传递给它们。须知我们希望在 worker 所分配的线程中使用接收者,所以将在闭包中引用 `receiver` 参数。示例 20-17 中展示的代码还不能编译:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -312,9 +312,9 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
|
|||||||
|
|
||||||
<span class="caption">示例 20-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在信道中发出任务</span>
|
<span class="caption">示例 20-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在信道中发出任务</span>
|
||||||
|
|
||||||
在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从信道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。
|
在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从信道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在它们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。
|
||||||
|
|
||||||
不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行他们。如示例 20-20 对 `Worker::new` 做出修改:
|
不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行它们。如示例 20-20 对 `Worker::new` 做出修改:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
示例 20-20 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 <span class="keystroke">ctrl-c</span> 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。
|
示例 20-20 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 <span class="keystroke">ctrl-c</span> 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。
|
||||||
|
|
||||||
现在我们要为 `ThreadPool` 实现 `Drop` trait 对线程池中的每一个线程调用 `join`,这样这些线程将会执行完他们的请求。接着会为 `ThreadPool` 实现一个告诉线程他们应该停止接收新请求并结束的方式。为了实践这些代码,修改 server 在优雅停机(graceful shutdown)之前只接受两个请求。
|
现在我们要为 `ThreadPool` 实现 `Drop` trait 对线程池中的每一个线程调用 `join`,这样这些线程将会执行完它们的请求。接着会为 `ThreadPool` 实现一个告诉线程它们应该停止接收新请求并结束的方式。为了实践这些代码,修改 server 在优雅停机(graceful shutdown)之前只接受两个请求。
|
||||||
|
|
||||||
### 为 `ThreadPool` 实现 `Drop` Trait
|
### 为 `ThreadPool` 实现 `Drop` Trait
|
||||||
|
|
||||||
现在开始为线程池实现 `Drop`。当线程池被丢弃时,应该 join 所有线程以确保他们完成其操作。示例 20-22 展示了 `Drop` 实现的第一次尝试;这些代码还不能够编译:
|
现在开始为线程池实现 `Drop`。当线程池被丢弃时,应该 join 所有线程以确保它们完成其操作。示例 20-22 展示了 `Drop` 实现的第一次尝试;这些代码还不能够编译:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
### 向线程发送信号使其停止接收任务
|
### 向线程发送信号使其停止接收任务
|
||||||
|
|
||||||
有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为他们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool`,则主线程会永远阻塞在等待第一个线程结束上。
|
有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为它们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool`,则主线程会永远阻塞在等待第一个线程结束上。
|
||||||
|
|
||||||
为了修复这个问题,我们将修改 `ThreadPool` 的 `drop` 实现并修改 `Worker` 循环。
|
为了修复这个问题,我们将修改 `ThreadPool` 的 `drop` 实现并修改 `Worker` 循环。
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user