Merge pull request #3 from KaiserY/master

change list translation
This commit is contained in:
Junjie Huang 2017-10-08 14:08:29 +08:00 committed by GitHub
commit ef0de4d808
42 changed files with 410 additions and 410 deletions

View File

@ -59,7 +59,7 @@ Hello, world!
## 处理一次猜测 ## 处理一次猜测
程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,允许用户输入猜测。在 *src/main.rs* 中输入列表 2-1 中的代码。 程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,允许用户输入猜测。在 *src/main.rs* 中输入示例 2-1 中的代码。
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -80,7 +80,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 2-1获取用户猜测并打印的代码</span> <span class="caption">示例 2-1获取用户猜测并打印的代码</span>
这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io`(输入/输出)库引入当前作用域。`io` 库来自于标准库(也被称为`std` 这些代码包含很多信息,我们一点一点地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io`(输入/输出)库引入当前作用域。`io` 库来自于标准库(也被称为`std`
@ -265,7 +265,7 @@ rand = "0.3.14"
[semver]: http://semver.org [semver]: http://semver.org
现在,不修改任何代码,构建项目,如列表 2-2 所示: 现在,不修改任何代码,构建项目,如示例 2-2 所示:
```text ```text
$ cargo build $ cargo build
@ -277,7 +277,7 @@ $ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
``` ```
<span class="caption">列表 2-2: 增加 rand crate 作为依赖之后运行 `cargo build` 的输出</span> <span class="caption">示例 2-2: 增加 rand crate 作为依赖之后运行 `cargo build` 的输出</span>
可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。 可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同。
@ -336,7 +336,7 @@ rand = "0.4.0"
### 生成一个随机数 ### 生成一个随机数
让我们开始 **使用** `rand`。下一步是更新 *src/main.rs*,如列表 2-3 所示: 让我们开始 **使用** `rand`。下一步是更新 *src/main.rs*,如示例 2-3 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -364,7 +364,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 2-3为了生成随机数而做的修改</span> <span class="caption">示例 2-3为了生成随机数而做的修改</span>
这里在顶部增加一行 `extern crate rand;` 通知 Rust 我们要使用外部依赖。这也会调用相应的 `use rand`,所以现在可以使用 `rand::` 前缀来调用 `rand` crate 中的任何内容。 这里在顶部增加一行 `extern crate rand;` 通知 Rust 我们要使用外部依赖。这也会调用相应的 `use rand`,所以现在可以使用 `rand::` 前缀来调用 `rand` crate 中的任何内容。
@ -400,7 +400,7 @@ You guessed: 5
## 比较猜测与秘密数字 ## 比较猜测与秘密数字
现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如列表 2-4 所示: 现在有了用户输入和一个随机数,我们可以比较他们。这个步骤如示例 2-4 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -435,7 +435,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 2-4处理比较两个数字可能的返回值</span> <span class="caption">示例 2-4处理比较两个数字可能的返回值</span>
新代码的第一行是另一个 `use`,从标准库引入了一个叫做 `std::cmp::Ordering` 的类型。`Ordering` 是一个像 `Result` 一样的枚举,不过它的成员是 `Less`、`Greater` 和 `Equal`。这是比较两个值时可能出现的三种结果。 新代码的第一行是另一个 `use`,从标准库引入了一个叫做 `std::cmp::Ordering` 的类型。`Ordering` 是一个像 `Result` 一样的枚举,不过它的成员是 `Less`、`Greater` 和 `Equal`。这是比较两个值时可能出现的三种结果。
@ -457,7 +457,7 @@ match guess.cmp(&secret_number) {
让我们看看使用 `match` 表达式的例子。假设用户猜了 50这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 `Ordering::Greater`**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式就此终止,因为该场景下没有检查最后一个分支的必要。 让我们看看使用 `match` 表达式的例子。假设用户猜了 50这时随机生成的秘密数字是 38。比较 50 与 38 时,因为 50 比 38 要大,`cmp` 方法会返回 `Ordering::Greater`。`Ordering::Greater` 是 `match` 表达式得到的值。它检查第一个分支的模式,`Ordering::Less` 与 `Ordering::Greater`并不匹配,所以它忽略了这个分支的动作并来到下一个分支。下一个分支的模式是 `Ordering::Greater`**正确** 匹配!这个分支关联的代码被执行,在屏幕打印出 `Too big!`。`match` 表达式就此终止,因为该场景下没有检查最后一个分支的必要。
然而,列表 2-4 的代码并不能编译,可以尝试一下: 然而,示例 2-4 的代码并不能编译,可以尝试一下:
```text ```text
$ cargo build $ cargo build
@ -713,7 +713,7 @@ You guessed: 61
You win! You win!
``` ```
太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 `println!`列表 2-5 为最终代码: 太棒了!再有最后一个小的修改,就能完成猜猜看游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 `println!`示例 2-5 为最终代码:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -756,7 +756,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 2-5猜猜看游戏的完整代码</span> <span class="caption">示例 2-5猜猜看游戏的完整代码</span>
## 总结 ## 总结

View File

@ -134,7 +134,7 @@ number is divisible by 3
#### 在 `let` 语句中使用 `if` #### 在 `let` 语句中使用 `if`
因为 `if` 是一个表达式,我们可以在 `let` 语句的右侧使用它,例如在列表 3-4 中: 因为 `if` 是一个表达式,我们可以在 `let` 语句的右侧使用它,例如在示例 3-4 中:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -151,7 +151,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 3-4`if` 的返回值赋值给一个变量</span> <span class="caption">示例 3-4`if` 的返回值赋值给一个变量</span>
`number` 变量将会绑定到基于 `if` 表达式结果的值。运行这段代码看看会出现什么: `number` 变量将会绑定到基于 `if` 表达式结果的值。运行这段代码看看会出现什么:
@ -162,7 +162,7 @@ $ cargo run
The value of number is: 5 The value of number is: 5
``` ```
还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个 `if` 表达式的值依赖哪个代码块被执行。这意味着 `if` 的每个分支的可能的返回值都必须是相同类型;在列表 3-4 中,`if` 分支和 `else` 分支的结果都是 `i32` 整型。不过如果像下面的例子那样这些类型并不匹配会怎么样呢? 还记得代码块的值是其最后一个表达式的值,以及数字本身也是一个表达式吗。在这个例子中,整个 `if` 表达式的值依赖哪个代码块被执行。这意味着 `if` 的每个分支的可能的返回值都必须是相同类型;在示例 3-4 中,`if` 分支和 `else` 分支的结果都是 `i32` 整型。不过如果像下面的例子那样这些类型并不匹配会怎么样呢?
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -282,7 +282,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 3-5使用 `while` 循环遍历集合中的每一个元素</span> <span class="caption">示例 3-5使用 `while` 循环遍历集合中的每一个元素</span>
这里代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素 这里代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素
@ -315,13 +315,13 @@ fn main() {
} }
``` ```
<span class="caption">列表 3-6使用 `for` 循环遍历集合中的每一个元素</span> <span class="caption">示例 3-6使用 `for` 循环遍历集合中的每一个元素</span>
当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 的机会。 当运行这段代码,将看到与示例 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 的机会。
例如,在列表 3-5 的代码中,如果从数组 `a` 中移除一个元素但忘记更新条件为 `while index < 4`,代码将会 panic。使用`for`循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。 例如,在示例 3-5 的代码中,如果从数组 `a` 中移除一个元素但忘记更新条件为 `while index < 4`,代码将会 panic。使用`for`循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。
`for` 循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如列表 3-5 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。 `for` 循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-5 中使用 `while` 循环的倒计时例子,大部分 Rustacean 也会使用 `for` 循环。这么做的方式是使用 `Range`,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。
下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range 下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range

View File

@ -52,7 +52,7 @@ Rust 的核心功能(之一)是 **所有权***ownership*)。虽然这
let s = "hello"; let s = "hello";
``` ```
变量`s`绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前 **作用域** 结束时都是有效的。列表 4-1 的注释标明了变量`s`在哪里是有效的: 变量`s`绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前 **作用域** 结束时都是有效的。示例 4-1 的注释标明了变量`s`在哪里是有效的:
```rust ```rust
{ // s is not valid here, its not yet declared { // s is not valid here, its not yet declared
@ -62,7 +62,7 @@ let s = "hello";
} // this scope is now over, and s is no longer valid } // this scope is now over, and s is no longer valid
``` ```
<span class="caption">列表 4-1一个变量和其有效的作用域</span> <span class="caption">示例 4-1一个变量和其有效的作用域</span>
换句话说,这里有两个重要的点: 换句话说,这里有两个重要的点:
@ -110,7 +110,7 @@ println!("{}", s); // This will print `hello, world!`
然而,第二部分实现起来就各有区别了。在有 **垃圾回收***garbage collector**GC*)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要 `allocate``free` 一一对应。 然而,第二部分实现起来就各有区别了。在有 **垃圾回收***garbage collector**GC*)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要 `allocate``free` 一一对应。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用 `String` 而不是字符串字面值的版本: Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是示例 4-1 中作用域例子的一个使用 `String` 而不是字符串字面值的版本:
```rust ```rust
{ {
@ -129,14 +129,14 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
#### 变量与数据交互的方式(一):移动 #### 变量与数据交互的方式(一):移动
Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子: Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看示例 4-2 中一个使用整型的例子:
```rust ```rust
let x = 5; let x = 5;
let y = x; let y = x;
``` ```
<span class="caption">列表 4-2将变量 `x` 赋值给 `y`</span> <span class="caption">示例 4-2将变量 `x` 赋值给 `y`</span>
根据其他语言的经验大致可以猜到这在干什么:“将 `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` 和 `y`,都等于 `5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。 根据其他语言的经验大致可以猜到这在干什么:“将 `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` 和 `y`,都等于 `5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。
@ -224,7 +224,7 @@ println!("s1 = {}, s2 = {}", s1, s2);
#### 只在栈上的数据:拷贝 #### 只在栈上的数据:拷贝
这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前列表 4-2 中的一部分: 这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前示例 4-2 中的一部分:
```rust ```rust
let x = 5; let x = 5;
@ -248,7 +248,7 @@ Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这
### 所有权与函数 ### 所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子: 将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。示例 4-7 是一个带有变量何时进入和离开作用域标注的例子:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -277,13 +277,13 @@ fn makes_copy(some_integer: i32) { // some_integer comes into scope.
} // Here, some_integer goes out of scope. Nothing special happens. } // Here, some_integer goes out of scope. Nothing special happens.
``` ```
<span class="caption">列表 4-7带有所有权和作用域标注的函数</span> <span class="caption">示例 4-7带有所有权和作用域标注的函数</span>
当尝试在调用 `takes_ownership` 后使用 `s`Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 `main` 函数中添加使用 `s``x` 的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。 当尝试在调用 `takes_ownership` 后使用 `s`Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 `main` 函数中添加使用 `s``x` 的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。
### 返回值与作用域 ### 返回值与作用域
返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子: 返回值也可以转移作用域。这里是一个有与示例 4-7 中类似标注的例子:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>

View File

@ -58,7 +58,7 @@ fn calculate_length(s: &String) -> usize { // s is a reference to a String
我们将获取引用作为函数参数称为 **借用***borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。 我们将获取引用作为函数参数称为 **借用***borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通! 如果我们尝试修改借用的变量呢?尝试示例 4-9 中的代码。剧透:这行不通!
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -74,7 +74,7 @@ fn change(some_string: &String) {
} }
``` ```
<span class="caption">列表 4-9尝试修改借用的值</span> <span class="caption">示例 4-9尝试修改借用的值</span>
这里是错误: 这里是错误:
@ -90,7 +90,7 @@ error: cannot borrow immutable borrowed content `*some_string` as mutable
### 可变引用 ### 可变引用
可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中: 可以通过一个小调整来修复在示例 4-9 代码中的错误,在示例 4-9 的代码中:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>

View File

@ -14,7 +14,7 @@
fn first_word(s: &String) -> ? fn first_word(s: &String) -> ?
``` ```
`first_word` 这个函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码: `first_word` 这个函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如示例 4-10 所示的代码:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -32,7 +32,7 @@ fn first_word(s: &String) -> usize {
} }
``` ```
<span class="caption">列表 4-10`first_word` 函数返回 `String` 参数的一个字节索引值</span> <span class="caption">示例 4-10`first_word` 函数返回 `String` 参数的一个字节索引值</span>
让我们将代码分解成小块。因为需要一个元素一个元素的检查 `String` 中的值是否是空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组: 让我们将代码分解成小块。因为需要一个元素一个元素的检查 `String` 中的值是否是空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组:
@ -60,7 +60,7 @@ for (i, &item) in bytes.iter().enumerate() {
s.len() s.len()
``` ```
现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单独一个 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 中 `first_word` 函数的程序: 现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单独一个 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下示例 4-11 中使用了示例 4-10 中 `first_word` 函数的程序:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -89,7 +89,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 4-11储存 `first_word` 函数调用的返回值并接着改变 `String` 的内容</span> <span class="caption">示例 4-11储存 `first_word` 函数调用的返回值并接着改变 `String` 的内容</span>
这个程序编译时没有任何错误,而且在调用 `s.clear()` 之后使用 `word` 也不会出错。这时 `word``s` 状态就没有联系了,所以 `word `仍然包含值 `5`。可以尝试用值 `5` 来提取变量 `s` 的第一个单词,不过这是有 bug 的,因为在我们将 `5` 保存到 `word` 之后 `s` 的内容已经改变。 这个程序编译时没有任何错误,而且在调用 `s.clear()` 之后使用 `word` 也不会出错。这时 `word``s` 状态就没有联系了,所以 `word `仍然包含值 `5`。可以尝试用值 `5` 来提取变量 `s` 的第一个单词,不过这是有 bug 的,因为在我们将 `5` 保存到 `word` 之后 `s` 的内容已经改变。
@ -173,7 +173,7 @@ fn first_word(s: &String) -> &str {
} }
``` ```
我们使用跟列表 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。 我们使用跟示例 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。
现在当调用 `first_word` 时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。 现在当调用 `first_word` 时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。
@ -183,7 +183,7 @@ fn first_word(s: &String) -> &str {
fn second_word(s: &String) -> &str { fn second_word(s: &String) -> &str {
``` ```
现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向 `String` 的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗那些代码逻辑上是不正确的不过却没有表现出任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误: 现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向 `String` 的引用保持有效。还记得示例 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗那些代码逻辑上是不正确的不过却没有表现出任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>

View File

@ -6,7 +6,7 @@
我们在第三章讨论过,结构体与元组类似。就像元组,结构体的每一部分可以是不同类型。不同于元组,需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字使得结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。 我们在第三章讨论过,结构体与元组类似。就像元组,结构体的每一部分可以是不同类型。不同于元组,需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字使得结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
为了定义结构体,通过 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作 **字段***field*),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体: 为了定义结构体,通过 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作 **字段***field*),并定义字段类型。例如,示例 5-1 展示了一个储存用户账号信息的结构体:
```rust ```rust
struct User { struct User {
@ -17,9 +17,9 @@ struct User {
} }
``` ```
<span class="caption">列表 5-1`User` 结构体定义</span> <span class="caption">示例 5-1`User` 结构体定义</span>
一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像列表 5-2 这样来声明一个特定的用户: 一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户:
```rust ```rust
# struct User { # struct User {
@ -37,9 +37,9 @@ let user1 = User {
}; };
``` ```
<span class="caption">列表 5-2创建 `User` 结构体的实例</span> <span class="caption">示例 5-2创建 `User` 结构体的实例</span>
为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用 `user1.email`。要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并对对应的字段赋值。列表 5-3 展示了如何改变一个可变的 `User` 实例 `email` 字段的值: 为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用 `user1.email`。要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并对对应的字段赋值。示例 5-3 展示了如何改变一个可变的 `User` 实例 `email` 字段的值:
```rust ```rust
# struct User { # struct User {
@ -59,7 +59,7 @@ let mut user1 = User {
user1.email = String::from("anotheremail@example.com"); user1.email = String::from("anotheremail@example.com");
``` ```
<span class="caption">列表 5-3改变 `User` 结构体 `email` 字段的值</span> <span class="caption">示例 5-3改变 `User` 结构体 `email` 字段的值</span>
与其他任何表达式一样,我们可以在函数体的最后一个表达式构造一个结构体,从函数隐式的返回一个结构体的新实例。表 5-4 显示了一个返回带有给定的 `email``username``User` 结构体的实例的 `build_user` 函数。`active` 字段的值为 `true`,并且 `sign_in_count` 的值为 `1` 与其他任何表达式一样,我们可以在函数体的最后一个表达式构造一个结构体,从函数隐式的返回一个结构体的新实例。表 5-4 显示了一个返回带有给定的 `email``username``User` 结构体的实例的 `build_user` 函数。`active` 字段的值为 `true`,并且 `sign_in_count` 的值为 `1`
@ -81,7 +81,7 @@ fn build_user(email: String, username: String) -> User {
} }
``` ```
<span class="caption">列表 5-4`build_user` 函数获取 email 和用户名并返回 `User` 实例</span> <span class="caption">示例 5-4`build_user` 函数获取 email 和用户名并返回 `User` 实例</span>
不过,重复 `email` 字段与 `email` 变量的名字,同样的对于`username`,感觉有一点无趣。将函数参数起与结构体字段相同的名字是可以理解的,但是如果结构体有更多字段,重复他们是十分烦人的。幸运的是,这里有一个方便的语法! 不过,重复 `email` 字段与 `email` 变量的名字,同样的对于`username`,感觉有一点无趣。将函数参数起与结构体字段相同的名字是可以理解的,但是如果结构体有更多字段,重复他们是十分烦人的。幸运的是,这里有一个方便的语法!
@ -89,7 +89,7 @@ fn build_user(email: String, username: String) -> User {
如果有变量与字段同名的话,你可以使用 **字段初始化语法***field init shorthand*)。这可以让创建新的结构体实例的函数更为简练。 如果有变量与字段同名的话,你可以使用 **字段初始化语法***field init shorthand*)。这可以让创建新的结构体实例的函数更为简练。
列表 5-4 中,名为 `email``username` 的参数与结构体 `User` 的字段 `email``username` 同名。因为名字相同,我们可以写出不重复 `email``username``build_user` 函数,如列表 5-5 所示。 这个版本的函数与列表 5-4 中代码的行为完全相同。这个字段初始化语法可以让这类代码更简洁,特别是当结构体有很多字段的时候。 示例 5-4 中,名为 `email``username` 的参数与结构体 `User` 的字段 `email``username` 同名。因为名字相同,我们可以写出不重复 `email``username``build_user` 函数,如示例 5-5 所示。 这个版本的函数与示例 5-4 中代码的行为完全相同。这个字段初始化语法可以让这类代码更简洁,特别是当结构体有很多字段的时候。
```rust ```rust
# struct User { # struct User {
@ -109,11 +109,11 @@ fn build_user(email: String, username: String) -> User {
} }
``` ```
<span class="caption">列表 5-5`build_user` 函数使用了字段初始化语法,因为 `email``username` 参数与结构体字段同名</span> <span class="caption">示例 5-5`build_user` 函数使用了字段初始化语法,因为 `email``username` 参数与结构体字段同名</span>
### 使用结构体更新语法从其他对象创建对象 ### 使用结构体更新语法从其他对象创建对象
可以从老的对象创建新的对象常常是很有帮助的,即复用大部分老对象的值并只改变一部分。列表 5-6 展示了一个设置 `email``username` 的值但其余字段使用与列表 5-2 中 `user1` 实例相同的值以创建新的 `User` 实例 `user2` 的例子: 可以从老的对象创建新的对象常常是很有帮助的,即复用大部分老对象的值并只改变一部分。示例 5-6 展示了一个设置 `email``username` 的值但其余字段使用与示例 5-2 中 `user1` 实例相同的值以创建新的 `User` 实例 `user2` 的例子:
```rust ```rust
# struct User { # struct User {
@ -138,9 +138,9 @@ let user2 = User {
}; };
``` ```
<span class="caption">列表 5-6创建 `User` 新实例,`user2`,并将一些字段的值设置为 `user1` 同名字段的值</span> <span class="caption">示例 5-6创建 `User` 新实例,`user2`,并将一些字段的值设置为 `user1` 同名字段的值</span>
**结构体更新语法***struct update syntax*)可以利用更少的代码获得与列表 5-6 相同的效果。结构体更新语法利用 `..` 以指定未显式设置的字段应有与给定实例对应字段相同的值。列表 5-7 中的代码同样地创建了有着不同的 `email``username` 值但 `active``sign_in_count` 字段与 `user1` 相同的实例 `user2` **结构体更新语法***struct update syntax*)可以利用更少的代码获得与示例 5-6 相同的效果。结构体更新语法利用 `..` 以指定未显式设置的字段应有与给定实例对应字段相同的值。示例 5-7 中的代码同样地创建了有着不同的 `email``username` 值但 `active``sign_in_count` 字段与 `user1` 相同的实例 `user2`
```rust ```rust
# struct User { # struct User {
@ -164,7 +164,7 @@ let user2 = User {
}; };
``` ```
<span class="caption">列表 5-7使用结构体更新语法为 `User` 实例设置新的 `email``username` 值,但使用 `user1` 变量中剩下字段的值</span> <span class="caption">示例 5-7使用结构体更新语法为 `User` 实例设置新的 `email``username` 值,但使用 `user1` 变量中剩下字段的值</span>
### 使用没有命名字段的元组结构体创建不同的类型 ### 使用没有命名字段的元组结构体创建不同的类型
@ -186,7 +186,7 @@ let origin = Point(0, 0, 0);
> ## 结构体数据的所有权 > ## 结构体数据的所有权
> >
> 在列表 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。 > 在示例 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。
> >
> 可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期***lifetimes*),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样: > 可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期***lifetimes*),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样:
> >

View File

@ -6,7 +6,7 @@
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。 为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。
使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 *src/main.rs* 文件中为此实现的一个小程序: 使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。示例 5-2 中是项目的 *src/main.rs* 文件中为此实现的一个小程序:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -26,7 +26,7 @@ fn area(length: u32, width: u32) -> u32 {
} }
``` ```
<span class="caption">列表 5-8通过指定长方形的长宽变量来计算长方形面积</span> <span class="caption">示例 5-8通过指定长方形的长宽变量来计算长方形面积</span>
尝试使用 `cargo run` 运行程序: 尝试使用 `cargo run` 运行程序:
@ -36,7 +36,7 @@ The area of the rectangle is 1500 square pixels.
### 使用元组重构 ### 使用元组重构
虽然列表 5-8 可以运行,并调用 `area` 函数用长方形的每个维度来计算出面积,不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。 虽然示例 5-8 可以运行,并调用 `area` 函数用长方形的每个维度来计算出面积,不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。
这个做法的问题突显在 `area` 的签名上: 这个做法的问题突显在 `area` 的签名上:
@ -44,7 +44,7 @@ The area of the rectangle is 1500 square pixels.
fn area(length: u32, width: u32) -> u32 { fn area(length: u32, width: u32) -> u32 {
``` ```
函数 `area` 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。第三章的 “将值组合进元组” 部分已经讨论过了一种可行的方法:元组。列表 5-9 是另一个使用元组的版本: 函数 `area` 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。第三章的 “将值组合进元组” 部分已经讨论过了一种可行的方法:元组。示例 5-9 是另一个使用元组的版本:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -63,7 +63,7 @@ fn area(dimensions: (u32, u32)) -> u32 {
} }
``` ```
<span class="caption">列表 5-8使用元组来指定长方形的长宽</span> <span class="caption">示例 5-8使用元组来指定长方形的长宽</span>
在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用 `area` 的时候只用传递一个参数。不过在另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: 在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用 `area` 的时候只用传递一个参数。不过在另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:
@ -71,7 +71,7 @@ fn area(dimensions: (u32, u32)) -> u32 {
### 使用结构体重构:增加更多意义 ### 使用结构体重构:增加更多意义
现在引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-10 所示: 现在引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如示例 5-10 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -95,7 +95,7 @@ fn area(rectangle: &Rectangle) -> u32 {
} }
``` ```
<span class="caption">列表 5-10定义 `Rectangle` 结构体</span> <span class="caption">示例 5-10定义 `Rectangle` 结构体</span>
这里我们定义了一个结构体并称其为 `Rectangle`。在 `{}` 中定义了字段 `length``width`,都是 `u32` 类型的。接着在 `main` 中,我们创建了一个长度为 50 和宽度为 30 的 `Rectangle` 的具体实例。 这里我们定义了一个结构体并称其为 `Rectangle`。在 `{}` 中定义了字段 `length``width`,都是 `u32` 类型的。接着在 `main` 中,我们创建了一个长度为 50 和宽度为 30 的 `Rectangle` 的具体实例。
@ -105,7 +105,7 @@ fn area(rectangle: &Rectangle) -> u32 {
### 通过衍生 trait 增加实用功能 ### 通过衍生 trait 增加实用功能
如果能够在调试程序时打印出 `Rectangle` 实例来查看其所有字段的值就更好了。列表 5-11 像往常一样使用 `println!` 宏: 如果能够在调试程序时打印出 `Rectangle` 实例来查看其所有字段的值就更好了。示例 5-11 像往常一样使用 `println!` 宏:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -122,7 +122,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 5-11尝试打印出 `Rectangle` 实例</span> <span class="caption">示例 5-11尝试打印出 `Rectangle` 实例</span>
如果运行代码,会出现带有如下核心信息的错误: 如果运行代码,会出现带有如下核心信息的错误:
@ -154,7 +154,7 @@ note: `Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it crate, add `#[derive(Debug)]` or manually implement it
``` ```
Rust **确实** 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上 `#[derive(Debug)]` 注解,如列表 5-12 所示: Rust **确实** 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上 `#[derive(Debug)]` 注解,如示例 5-12 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -172,7 +172,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 5-12增加注解来导出 `Debug` trait </span> <span class="caption">示例 5-12增加注解来导出 `Debug` trait </span>
此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出: 此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出:

View File

@ -8,7 +8,7 @@
### 定义方法 ### 定义方法
让我们将获取一个 `Rectangle` 实例作为参数的 `area` 函数改写成一个定义于 `Rectangle` 结构体上的 `area` 方法,如列表 5-13 所示: 让我们将获取一个 `Rectangle` 实例作为参数的 `area` 函数改写成一个定义于 `Rectangle` 结构体上的 `area` 方法,如示例 5-13 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -35,7 +35,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 5-13`Rectangle` 结构体上定义 `area` 方法</span> <span class="caption">示例 5-13`Rectangle` 结构体上定义 `area` 方法</span>
为了使函数定义于 `Rectangle` 的上下文中,我们开始了一个 `impl` 块(`impl` 是 *implementation* 的缩写)。接着将函数移动到 `impl` 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 `self`。然后在`main` 中将我们调用 `area` 方法并传递 `rect1` 作为参数的地方,改成使用 **方法语法***method syntax*)在 `Rectangle` 实例上调用 `area` 方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。 为了使函数定义于 `Rectangle` 的上下文中,我们开始了一个 `impl` 块(`impl` 是 *implementation* 的缩写)。接着将函数移动到 `impl` 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 `self`。然后在`main` 中将我们调用 `area` 方法并传递 `rect1` 作为参数的地方,改成使用 **方法语法***method syntax*)在 `Rectangle` 实例上调用 `area` 方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。
@ -78,7 +78,7 @@ fn main() {
### 带有更多参数的方法 ### 带有更多参数的方法
让我们更多的实践一下方法,通过为 `Rectangle` 结构体实现第二个方法。这回,我们让一个 `Rectangle` 的实例获取另一个 `Rectangle` 实例并返回 `self` 能否完全包含第二个长方形,如果能返回 `true` 若不能则返回 `false`。一旦定义了 `can_hold` 方法,就可以运行列表 5-14 中的代码了: 让我们更多的实践一下方法,通过为 `Rectangle` 结构体实现第二个方法。这回,我们让一个 `Rectangle` 的实例获取另一个 `Rectangle` 实例并返回 `self` 能否完全包含第二个长方形,如果能返回 `true` 若不能则返回 `false`。一旦定义了 `can_hold` 方法,就可以运行示例 5-14 中的代码了:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -93,7 +93,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 5-14展示还未实现的 `can_hold` 方法的应用</span> <span class="caption">示例 5-14展示还未实现的 `can_hold` 方法的应用</span>
我们希望看到如下输出,因为 `rect2` 的长宽都小于 `rect1`,而 `rect3``rect1` 要宽: 我们希望看到如下输出,因为 `rect2` 的长宽都小于 `rect1`,而 `rect3``rect1` 要宽:
@ -102,7 +102,7 @@ Can rect1 hold rect2? true
Can rect1 hold rect3? false Can rect1 hold rect3? false
``` ```
因为我们想定义一个方法,所以它应该位于 `impl Rectangle` 块中。方法名是 `can_hold`,并且它会获取另一个 `Rectangle` 的不可变借用作为参数。通过观察调用位置的代码可以看出参数是什么类型的:`rect1.can_hold(&rect2)` 传入了 `&rect2`,它是一个 `Rectangle` 的实例 `rect2` 的不可变借用。这是可以理解的,因为我们只需要读取 `rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望 `main` 保持 `rect2` 的所有权这样就可以在调用这个方法后继续使用它。`can_hold` 的返回值是一个布尔值,其实现会分别检查 `self` 的长宽是否都大于另一个 `Rectangle`。让我们在列表 5-13 的 `impl` 块中增加这个新方法,如列表 5-15 所示: 因为我们想定义一个方法,所以它应该位于 `impl Rectangle` 块中。方法名是 `can_hold`,并且它会获取另一个 `Rectangle` 的不可变借用作为参数。通过观察调用位置的代码可以看出参数是什么类型的:`rect1.can_hold(&rect2)` 传入了 `&rect2`,它是一个 `Rectangle` 的实例 `rect2` 的不可变借用。这是可以理解的,因为我们只需要读取 `rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望 `main` 保持 `rect2` 的所有权这样就可以在调用这个方法后继续使用它。`can_hold` 的返回值是一个布尔值,其实现会分别检查 `self` 的长宽是否都大于另一个 `Rectangle`。让我们在示例 5-13 的 `impl` 块中增加这个新方法,如示例 5-15 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -124,9 +124,9 @@ impl Rectangle {
} }
``` ```
<span class="caption">列表 5-15`Rectangle` 上实现 `can_hold` 方法,它获取另一个 `Rectangle` 实例作为参数</span> <span class="caption">示例 5-15`Rectangle` 上实现 `can_hold` 方法,它获取另一个 `Rectangle` 实例作为参数</span>
如果结合列表 5-14 的 `main` 函数来运行,就会看到想要得到的输出。方法可以在 `self` 后增加多个参数,而且这些参数就像函数中的参数一样工作。 如果结合示例 5-14 的 `main` 函数来运行,就会看到想要得到的输出。方法可以在 `self` 后增加多个参数,而且这些参数就像函数中的参数一样工作。
### 关联函数 ### 关联函数
@ -154,7 +154,7 @@ impl Rectangle {
### 多个 `impl` ### 多个 `impl`
每个结构体都允许拥有多个 `impl` 块。例如,列表 5-15 等同于列表 5-16 的代码,这里每个方法有其自己的 `impl` 块: 每个结构体都允许拥有多个 `impl` 块。例如,示例 5-15 等同于示例 5-16 的代码,这里每个方法有其自己的 `impl` 块:
```rust ```rust
# #[derive(Debug)] # #[derive(Debug)]
@ -176,7 +176,7 @@ impl Rectangle {
} }
``` ```
<span class="caption">列表 5-16使用多个 `impl` 块重写列表 5-15</span> <span class="caption">示例 5-16使用多个 `impl` 块重写示例 5-15</span>
## 总结 ## 总结

View File

@ -58,7 +58,7 @@ route(IpAddrKind::V4);
route(IpAddrKind::V6); route(IpAddrKind::V6);
``` ```
使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址 **数据** 的方法;只知道它是什么 **类型** 的。考虑到已经在第五章学习过结构体了,你可能会像列表 6-1 那样修改这个问题: 使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址 **数据** 的方法;只知道它是什么 **类型** 的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样修改这个问题:
```rust ```rust
enum IpAddrKind { enum IpAddrKind {
@ -82,7 +82,7 @@ let loopback = IpAddr {
}; };
``` ```
<span class="caption">列表 6-1将 IP 地址的数据和 `IpAddrKind` 成员储存在一个 `struct`</span> <span class="caption">示例 6-1将 IP 地址的数据和 `IpAddrKind` 成员储存在一个 `struct`</span>
这里我们定义了一个有两个字段的结构体 `IpAddr``kind` 字段是 `IpAddrKind`(之前定义的枚举)类型的而 `address` 字段是 `String` 类型的。这里有两个结构体的实例。第一个,`home`,它的 `kind` 的值是 `IpAddrKind::V4` 与之相关联的地址数据是 `127.0.0.1`。第二个实例,`loopback``kind` 的值是 `IpAddrKind` 的另一个成员,`V6`,关联的地址是 `::1`。我们使用了一个结构体来将 `kind``address` 打包在一起,现在枚举成员就与值相关联了。 这里我们定义了一个有两个字段的结构体 `IpAddr``kind` 字段是 `IpAddrKind`(之前定义的枚举)类型的而 `address` 字段是 `String` 类型的。这里有两个结构体的实例。第一个,`home`,它的 `kind` 的值是 `IpAddrKind::V4` 与之相关联的地址数据是 `127.0.0.1`。第二个实例,`loopback``kind` 的值是 `IpAddrKind` 的另一个成员,`V6`,关联的地址是 `::1`。我们使用了一个结构体来将 `kind``address` 打包在一起,现在枚举成员就与值相关联了。
@ -137,7 +137,7 @@ enum IpAddr {
注意虽然标准库中包含一个 `IpAddr` 的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。 注意虽然标准库中包含一个 `IpAddr` 的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型: 来看看示例 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
```rust ```rust
enum Message { enum Message {
@ -148,7 +148,7 @@ enum Message {
} }
``` ```
<span class="caption">列表 6-2一个 `Message` 枚举,其每个成员都储存了不同数量和类型的值</span> <span class="caption">示例 6-2一个 `Message` 枚举,其每个成员都储存了不同数量和类型的值</span>
这个枚举有四个含有不同类型的成员: 这个枚举有四个含有不同类型的成员:
@ -157,7 +157,7 @@ enum Message {
* `Write` 包含单独一个 `String` * `Write` 包含单独一个 `String`
* `ChangeColor` 包含三个 `i32` * `ChangeColor` 包含三个 `i32`
定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用 `struct` 关键字而且所有成员都被组合在一起位于 `Message` 下之外。如下这些结构体可以包含与之前枚举成员中相同的数据: 定义一个像示例 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用 `struct` 关键字而且所有成员都被组合在一起位于 `Message` 下之外。如下这些结构体可以包含与之前枚举成员中相同的数据:
```rust ```rust
struct QuitMessage; // unit struct struct QuitMessage; // unit struct
@ -169,7 +169,7 @@ struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct
``` ```
不过如果我们使用不同的结构体,他们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用列表 6-2 中定义的 `Message` 枚举那样,因为他们是一个类型的。 不过如果我们使用不同的结构体,他们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用示例 6-2 中定义的 `Message` 枚举那样,因为他们是一个类型的。
结构体和枚举还有另一个相似点:就像可以使用 `impl` 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 `Message` 枚举上的叫做 `call` 的方法: 结构体和枚举还有另一个相似点:就像可以使用 `impl` 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 `Message` 枚举上的叫做 `call` 的方法:

View File

@ -8,7 +8,7 @@ Rust 有一个叫做 `match` 的极为强大的控制流运算符,它允许我
可以把 `match` 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查 `match` 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。 可以把 `match` 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查 `match` 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。
因为刚刚提到了硬币,让我们用他们来作为一个使用 `match` 的例子!我们可以编写一个函数来获取一个未知的(美帝)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示: 因为刚刚提到了硬币,让我们用他们来作为一个使用 `match` 的例子!我们可以编写一个函数来获取一个未知的(美帝)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如示例 6-3 中所示:
```rust ```rust
enum Coin { enum Coin {
@ -28,17 +28,17 @@ fn value_in_cents(coin: Coin) -> u32 {
} }
``` ```
<span class="caption">列表 6-3一个枚举和一个以枚举成员作为模式的 `match` 表达式</span> <span class="caption">示例 6-3一个枚举和一个以枚举成员作为模式的 `match` 表达式</span>
拆开 `value_in_cents` 函数中的 `match` 来看。首先,我们列出 `match` 关键字后跟一个表达式,在这个例子中是 `coin` 的值。这看起来非常像 `if` 使用的表达式,不过这里有一个非常大的区别:对于 `if`,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的 `coin` 的类型是列表 6-3 中定义的 `Coin` 枚举。 拆开 `value_in_cents` 函数中的 `match` 来看。首先,我们列出 `match` 关键字后跟一个表达式,在这个例子中是 `coin` 的值。这看起来非常像 `if` 使用的表达式,不过这里有一个非常大的区别:对于 `if`,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的 `coin` 的类型是示例 6-3 中定义的 `Coin` 枚举。
接下来是 `match` 的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值 `Coin::Penny` 而之后的 `=>` 运算符将模式和将要运行的代码分开。这里的代码就仅仅是值 `1`。每一个分支之间使用逗号分隔。 接下来是 `match` 的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值 `Coin::Penny` 而之后的 `=>` 运算符将模式和将要运行的代码分开。这里的代码就仅仅是值 `1`。每一个分支之间使用逗号分隔。
`match` 表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支:列表 6-3 中的 `match` 有四个分支。 `match` 表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支:示例 6-3 中的 `match` 有四个分支。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 `match` 表达式的返回值。 每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 `match` 表达式的返回值。
如果分支代码较短的话通常不使用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用`Coin::Penny` 调用时都会打印出 “Lucky penny!”,同时仍然返回代码块最后的值,`1` 如果分支代码较短的话通常不使用大括号,正如示例 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用`Coin::Penny` 调用时都会打印出 “Lucky penny!”,同时仍然返回代码块最后的值,`1`
```rust ```rust
# enum Coin { # enum Coin {
@ -65,7 +65,7 @@ fn value_in_cents(coin: Coin) -> u32 {
匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。 匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。
作为一个例子让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的 `enum`,通过改变 `Quarter` 成员来包含一个 `State` 值,列表 6-4 中完成了这些修改: 作为一个例子让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的 `enum`,通过改变 `Quarter` 成员来包含一个 `State` 值,示例 6-4 中完成了这些修改:
```rust ```rust
#[derive(Debug)] // So we can inspect the state in a minute #[derive(Debug)] // So we can inspect the state in a minute
@ -83,7 +83,7 @@ enum Coin {
} }
``` ```
<span class="caption">列表 6-4`Quarter` 成员也存放了一个 `UsState` 值的 `Coin` 枚举</span> <span class="caption">示例 6-4`Quarter` 成员也存放了一个 `UsState` 值的 `Coin` 枚举</span>
想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。 想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。
@ -124,7 +124,7 @@ fn value_in_cents(coin: Coin) -> u32 {
比如我们想要编写一个函数,它获取一个 `Option<i32>` 并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回 `None` 值并不尝试执行任何操作。 比如我们想要编写一个函数,它获取一个 `Option<i32>` 并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回 `None` 值并不尝试执行任何操作。
得益于 `match`,编写这个函数非常简单,它将看起来像列表 6-5 中这样: 得益于 `match`,编写这个函数非常简单,它将看起来像示例 6-5 中这样:
```rust ```rust
fn plus_one(x: Option<i32>) -> Option<i32> { fn plus_one(x: Option<i32>) -> Option<i32> {
@ -139,7 +139,7 @@ let six = plus_one(five);
let none = plus_one(None); let none = plus_one(None);
``` ```
<span class="caption">列表 6-5一个在 `Option<i32>` 上使用 `match` 表达式的函数</span> <span class="caption">示例 6-5一个在 `Option<i32>` 上使用 `match` 表达式的函数</span>
#### 匹配 `Some(T)` #### 匹配 `Some(T)`
@ -159,7 +159,7 @@ Some(i) => Some(i + 1),
#### 匹配 `None` #### 匹配 `None`
接着考虑下列表 6-5 中 `plus_one` 的第二个调用,这里 `x``None`。我们进入 `match` 并与第一个分支相比较。 接着考虑下示例 6-5 中 `plus_one` 的第二个调用,这里 `x``None`。我们进入 `match` 并与第一个分支相比较。
```rust,ignore ```rust,ignore
None => None, None => None,

View File

@ -4,7 +4,7 @@
> <br> > <br>
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 > commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
`if let` 语法让我们以一种不那么冗长的方式结合 `if``let`,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个 `Option<u8>` 值并只希望当值为三时执行代码: `if let` 语法让我们以一种不那么冗长的方式结合 `if``let`,来处理匹配一个模式的值而忽略其他的值。考虑示例 6-6 中的程序,它匹配一个 `Option<u8>` 值并只希望当值为三时执行代码:
```rust ```rust
let some_u8_value = Some(0u8); let some_u8_value = Some(0u8);
@ -14,11 +14,11 @@ match some_u8_value {
} }
``` ```
<span class="caption">列表 6-6`match` 只关心当值为 `Some(3)` 时执行代码</span> <span class="caption">示例 6-6`match` 只关心当值为 `Some(3)` 时执行代码</span>
我们想要对 `Some(3)` 匹配进行操作不过不想处理任何其他 `Some<u8>` 值或 `None` 值。为了满足 `match` 表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 `_ => ()`,这样也要增加很多样板代码。 我们想要对 `Some(3)` 匹配进行操作不过不想处理任何其他 `Some<u8>` 值或 `None` 值。为了满足 `match` 表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 `_ => ()`,这样也要增加很多样板代码。
不过我们可以使用 `if let` 这种更短的方式编写。如下代码与列表 6-6 中的 `match` 行为一致: 不过我们可以使用 `if let` 这种更短的方式编写。如下代码与示例 6-6 中的 `match` 行为一致:
```rust ```rust
# let some_u8_value = Some(0u8); # let some_u8_value = Some(0u8);
@ -33,7 +33,7 @@ if let Some(3) = some_u8_value {
换句话说,可以认为 `if let``match` 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。 换句话说,可以认为 `if let``match` 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
可以在 `if let` 中包含一个 `else`。`else` 块中的代码与 `match` 表达式中的 `_` 分支块中的代码相同,这样的 `match` 表达式就等同于 `if let``else`。回忆一下列表 6-4 中 `Coin` 枚举的定义,它的 `Quarter` 成员包含一个 `UsState` 值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个 `match` 表达式: 可以在 `if let` 中包含一个 `else`。`else` 块中的代码与 `match` 表达式中的 `_` 分支块中的代码相同,这样的 `match` 表达式就等同于 `if let``else`。回忆一下示例 6-4 中 `Coin` 枚举的定义,它的 `Quarter` 成员包含一个 `UsState` 值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个 `match` 表达式:
```rust ```rust
# #[derive(Debug)] # #[derive(Debug)]

View File

@ -47,7 +47,7 @@ mod network {
`mod` 关键字的后面是模块的名字,`network`,接着是位于大括号中的代码块。代码块中的一切都位于 `network` 命名空间中。在这个例子中,只有一个函数,`connect`。如果想要在 `network` 模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法 `::`,像这样:`network::connect()`,而不是只是 `connect()` `mod` 关键字的后面是模块的名字,`network`,接着是位于大括号中的代码块。代码块中的一切都位于 `network` 命名空间中。在这个例子中,只有一个函数,`connect`。如果想要在 `network` 模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法 `::`,像这样:`network::connect()`,而不是只是 `connect()`
也可以在 *src/lib.rs* 文件中同时存在多个模块。例如,再拥有一个 `client` 模块,它也有一个叫做 `connect` 的函数,如列表 7-1 中所示那样增加这个模块: 也可以在 *src/lib.rs* 文件中同时存在多个模块。例如,再拥有一个 `client` 模块,它也有一个叫做 `connect` 的函数,如示例 7-1 中所示那样增加这个模块:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -63,11 +63,11 @@ mod client {
} }
``` ```
<span class="caption">列表 7-1`network` 模块和 `client` 一同定义于 *src/lib.rs*</span> <span class="caption">示例 7-1`network` 模块和 `client` 一同定义于 *src/lib.rs*</span>
现在我们有了 `network::connect` 函数和 `client::connect` 函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。 现在我们有了 `network::connect` 函数和 `client::connect` 函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
在这个例子中,因为我们构建的是一个库,作为库入口点的文件是 *src/lib.rs*。然而,对于创建模块来说,*src/lib.rs* 并没有什么特殊意义。也可以在二进制 crate 的 *src/main.rs* 中创建模块,正如在库 crate 的 *src/lib.rs* 创建模块一样。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client` 模块和它的函数 `connect` 可能放在 `network` 命名空间里显得更有道理,如列表 7-2 所示: 在这个例子中,因为我们构建的是一个库,作为库入口点的文件是 *src/lib.rs*。然而,对于创建模块来说,*src/lib.rs* 并没有什么特殊意义。也可以在二进制 crate 的 *src/main.rs* 中创建模块,正如在库 crate 的 *src/lib.rs* 创建模块一样。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client` 模块和它的函数 `connect` 可能放在 `network` 命名空间里显得更有道理,如示例 7-2 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -83,11 +83,11 @@ mod network {
} }
``` ```
<span class="caption">列表 7-2`client` 模块移动到 `network` 模块中</span> <span class="caption">示例 7-2`client` 模块移动到 `network` 模块中</span>
*src/lib.rs* 文件中,将现有的 `mod network``mod client` 的定义替换为列表 7-2 中的定义,这里将 `client` 模块作为 `network` 的一个内部模块。现在我们有了 `network::connect``network::client::connect` 函数:同样的,这两个 `connect` 函数也不相冲突,因为他们在不同的命名空间中。 *src/lib.rs* 文件中,将现有的 `mod network``mod client` 的定义替换为示例 7-2 中的定义,这里将 `client` 模块作为 `network` 的一个内部模块。现在我们有了 `network::connect``network::client::connect` 函数:同样的,这两个 `connect` 函数也不相冲突,因为他们在不同的命名空间中。
这样,模块之间形成了一个层次结构。*src/lib.rs* 的内容位于最顶层,而其子模块位于较低的层次。如下是列表 7-1 中的例子以层次的方式考虑的结构: 这样,模块之间形成了一个层次结构。*src/lib.rs* 的内容位于最顶层,而其子模块位于较低的层次。如下是示例 7-1 中的例子以层次的方式考虑的结构:
```text ```text
communicator communicator
@ -95,7 +95,7 @@ communicator
└── client └── client
``` ```
而这是列表 7-2 中例子的的层次结构: 而这是示例 7-2 中例子的的层次结构:
```text ```text
communicator communicator
@ -103,11 +103,11 @@ communicator
└── client └── client
``` ```
可以看到列表 7-2 中,`client` 是 `network` 的子模块,而不是它的同级模块。更为复杂的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中 “符合逻辑” 的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块,总有一个会是你会喜欢的结构。 可以看到示例 7-2 中,`client` 是 `network` 的子模块,而不是它的同级模块。更为复杂的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中 “符合逻辑” 的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块,总有一个会是你会喜欢的结构。
### 将模块移动到其他文件 ### 将模块移动到其他文件
位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不会是所有的内容都落到 *src/lib.rs**src/main.rs* 中了。为了举例,我们将从列表 7-3 中的代码开始: 位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不会是所有的内容都落到 *src/lib.rs**src/main.rs* 中了。为了举例,我们将从示例 7-3 中的代码开始:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -128,7 +128,7 @@ mod network {
} }
``` ```
<span class="caption">列表 7-3三个模块`client`、`network` 和 `network::server`,他们都定义于 *src/lib.rs*</span> <span class="caption">示例 7-3三个模块`client`、`network` 和 `network::server`,他们都定义于 *src/lib.rs*</span>
*src/lib.rs* 文件有如下层次结构: *src/lib.rs* 文件有如下层次结构:
@ -253,7 +253,7 @@ fn connect() {
} }
``` ```
当尝试运行 `cargo build` 时,会出现如列表 7-4 中所示的错误: 当尝试运行 `cargo build` 时,会出现如示例 7-4 中所示的错误:
```text ```text
$ cargo build $ cargo build
@ -276,11 +276,11 @@ note: ... or maybe `use` the module `server` instead of possibly redeclaring it
| ^^^^^^ | ^^^^^^
``` ```
<span class="caption">列表 7-4尝试将 `server` 子模块提取到 *src/server.rs* 时出现的错误</span> <span class="caption">示例 7-4尝试将 `server` 子模块提取到 *src/server.rs* 时出现的错误</span>
这个错误说明 “不能在这个位置新声明一个模块” 并指出 *src/network.rs* 中的 `mod server;` 这一行。看来 *src/network.rs**src/lib.rs* 在某些方面是不同的;继续阅读以理解这是为什么。 这个错误说明 “不能在这个位置新声明一个模块” 并指出 *src/network.rs* 中的 `mod server;` 这一行。看来 *src/network.rs**src/lib.rs* 在某些方面是不同的;继续阅读以理解这是为什么。
列表 7-4 中间的 note 事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作: 示例 7-4 中间的 note 事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:
```text ```text
note: maybe move this module `network` to its own directory via note: maybe move this module `network` to its own directory via
@ -301,7 +301,7 @@ $ mv src/network.rs src/network/mod.rs
$ mv src/server.rs src/network $ mv src/server.rs src/network
``` ```
现在如果运行 `cargo build` 的话将顺利编译(虽然仍有警告)。现在模块的布局看起来仍然与列表 7-3 中所有代码都在 *src/lib.rs* 中时完全一样: 现在如果运行 `cargo build` 的话将顺利编译(虽然仍有警告)。现在模块的布局看起来仍然与示例 7-3 中所有代码都在 *src/lib.rs* 中时完全一样:
```text ```text
communicator communicator

View File

@ -4,7 +4,7 @@
> <br> > <br>
> commit 0a4ed5875aeba78a81ae03ac73aeb84d2e2aca86 > commit 0a4ed5875aeba78a81ae03ac73aeb84d2e2aca86
我们通过将 `network``network::server` 的代码分别移动到 *src/network/mod.rs**src/network/server.rs* 文件中解决了列表 7-4 中出现的错误信息。现在,`cargo build` 能够构建我们的项目,不过仍然有一些警告信息,表示 `client::connect`、`network::connect` 和`network::server::connect` 函数没有被使用: 我们通过将 `network``network::server` 的代码分别移动到 *src/network/mod.rs**src/network/server.rs* 文件中解决了示例 7-4 中出现的错误信息。现在,`cargo build` 能够构建我们的项目,不过仍然有一些警告信息,表示 `client::connect`、`network::connect` 和`network::server::connect` 函数没有被使用:
```text ```text
warning: function is never used: `connect`, #[warn(dead_code)] on by default warning: function is never used: `connect`, #[warn(dead_code)] on by default
@ -169,7 +169,7 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
### 私有性示例 ### 私有性示例
让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 *src/lib.rs* 输入列表 7-5 中的代码: 让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 *src/lib.rs* 输入示例 7-5 中的代码:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -194,7 +194,7 @@ fn try_me() {
} }
``` ```
<span class="caption">列表 7-5私有和公有函数的例子其中部分是不正确的</span> <span class="caption">示例 7-5私有和公有函数的例子其中部分是不正确的</span>
在尝试编译这些代码之前,猜测一下 `try_me` 函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论! 在尝试编译这些代码之前,猜测一下 `try_me` 函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!

View File

@ -4,7 +4,7 @@
> <br> > <br>
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 > commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的 `nested_modules` 函数调用。 我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如示例 7-6 中所示的 `nested_modules` 函数调用。
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -22,7 +22,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 7-6通过完全指定模块中的路径来调用函数</span> <span class="caption">示例 7-6通过完全指定模块中的路径来调用函数</span>
如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。 如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。

View File

@ -133,7 +133,7 @@ let row = vec![
]; ];
``` ```
<span class="caption">列表 8-1定义一个枚举以便能在 vector 中存放不同类型的数据</span> <span class="caption">示例 8-1定义一个枚举以便能在 vector 中存放不同类型的数据</span>
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。 Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。

View File

@ -74,7 +74,7 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
这指向了一个不是我们编写的文件,*libcollections/vec.rs*。这是标准库中 `Vec<T>` 的实现。这是当对 vector `v` 使用 `[]`*libcollections/vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。 这指向了一个不是我们编写的文件,*libcollections/vec.rs*。这是标准库中 `Vec<T>` 的实现。这是当对 vector `v` 使用 `[]`*libcollections/vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。
接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出: 接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。示例 9-1 显示了其输出:
```text ```text
$ RUST_BACKTRACE=1 cargo run $ RUST_BACKTRACE=1 cargo run
@ -116,7 +116,7 @@ stack backtrace:
17: 0x0 - <unknown> 17: 0x0 - <unknown>
``` ```
<span class="caption">列表 9-1当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息</span> <span class="caption">示例 9-1当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息</span>
这里有大量的输出backtrace 第 11 行指向了我们程序中引起错误的行:*src/main.rs* 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。 这里有大量的输出backtrace 第 11 行指向了我们程序中引起错误的行:*src/main.rs* 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。

View File

@ -19,7 +19,7 @@ enum Result<T, E> {
`T``E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 `T``E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
让我们调用一个返回 `Result` 的函数,因为它可能会失败:如列表 9-2 所示打开一个文件: 让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-2 所示打开一个文件:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -31,7 +31,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 9-2打开文件</span> <span class="caption">示例 9-2打开文件</span>
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么,为此我们将 `let f` 语句改为: 如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么,为此我们将 `let f` 语句改为:
@ -59,7 +59,7 @@ error[E0308]: mismatched types
`File::open` 成功的情况下,变量 `f` 的值将会是一个包含文件句柄的 `Ok` 实例。在失败的情况下,`f` 会是一个包含更多关于出现了何种错误信息的 `Err` 实例。 `File::open` 成功的情况下,变量 `f` 的值将会是一个包含文件句柄的 `Ok` 实例。在失败的情况下,`f` 会是一个包含更多关于出现了何种错误信息的 `Err` 实例。
我们需要在列表 9-2 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。列表 9-3 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。 我们需要在示例 9-2 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-3 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -78,7 +78,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 9-3使用 `match` 表达式处理可能的 `Result` 成员</span> <span class="caption">示例 9-3使用 `match` 表达式处理可能的 `Result` 成员</span>
注意与 `Option` 枚举一样,`Result` 枚举和其成员也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok``Err` 之前指定 `Result::` 注意与 `Option` 枚举一样,`Result` 枚举和其成员也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok``Err` 之前指定 `Result::`
@ -93,7 +93,7 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:8
### 匹配不同的错误 ### 匹配不同的错误
列表 9-3 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样 `panic!`。让我们看看列表 9-4其中 `match` 增加了另一个分支: 示例 9-3 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像示例 9-3 那样 `panic!`。让我们看看示例 9-4其中 `match` 增加了另一个分支:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -127,7 +127,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 9-4使用不同的方式处理不同类型的错误</span> <span class="caption">示例 9-4使用不同的方式处理不同类型的错误</span>
`File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。 `File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。
@ -137,7 +137,7 @@ fn main() {
### 失败时 panic 的捷径:`unwrap` 和 `expect` ### 失败时 panic 的捷径:`unwrap` 和 `expect`
`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明意图。`Result<T, E>` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于列表 9-3 中的 `match` 语句。如果 `Result` 值是成员 `Ok``unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err``unwrap` 会为我们调用 `panic!` `match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明意图。`Result<T, E>` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-3 中的 `match` 语句。如果 `Result` 值是成员 `Ok``unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err``unwrap` 会为我们调用 `panic!`
```rust,should_panic ```rust,should_panic
use std::fs::File; use std::fs::File;
@ -177,7 +177,7 @@ thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 **传播***propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。 当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 **传播***propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码: 例如,示例 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
```rust ```rust
use std::io; use std::io;
@ -201,11 +201,11 @@ fn read_username_from_file() -> Result<String, io::Error> {
} }
``` ```
<span class="caption">列表 9-5一个函数使用 `match` 将错误返回给代码调用者</span> <span class="caption">示例 9-5一个函数使用 `match` 将错误返回给代码调用者</span>
首先让我们看看函数的返回值:`Result<String, io::Error>`。这意味着函数返回一个 `Result<T, E>` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String``Ok` 值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。 首先让我们看看函数的返回值:`Result<String, io::Error>`。这意味着函数返回一个 `Result<T, E>` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String``Ok` 值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。
函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于列表 9-3 中的 `match`,唯一的区别是不再当 `Err` 时调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。 函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-3 中的 `match`,唯一的区别是不再当 `Err` 时调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。
接着我们在变量 `s` 中创建了一个新 `String` 并调用文件句柄 `f``read_to_string` 方法来将文件的内容读取到 `s` 中。`read_to_string` 方法也返回一个 `Result` 因为它也可能会失败:哪怕是 `File::open` 已经成功了。所以我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进 `Ok``s` 中。如果`read_to_string` 失败了,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。并不需要显式的调用 `return`,因为这是函数的最后一个表达式。 接着我们在变量 `s` 中创建了一个新 `String` 并调用文件句柄 `f``read_to_string` 方法来将文件的内容读取到 `s` 中。`read_to_string` 方法也返回一个 `Result` 因为它也可能会失败:哪怕是 `File::open` 已经成功了。所以我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进 `Ok``s` 中。如果`read_to_string` 失败了,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。并不需要显式的调用 `return`,因为这是函数的最后一个表达式。
@ -215,7 +215,7 @@ fn read_username_from_file() -> Result<String, io::Error> {
### 传播错误的捷径:`?` ### 传播错误的捷径:`?`
列表 9-6 展示了一个 `read_username_from_file` 的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的: 示例 9-6 展示了一个 `read_username_from_file` 的实现,它实现了与示例 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的:
```rust ```rust
use std::io; use std::io;
@ -230,11 +230,11 @@ fn read_username_from_file() -> Result<String, io::Error> {
} }
``` ```
<span class="caption">列表 9-6一个使用 `?` 向调用者返回错误的函数</span> <span class="caption">示例 9-6一个使用 `?` 向调用者返回错误的函数</span>
`Result` 值之后的 `?` 被定义为与列表 9-5 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err``Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 `Result` 值之后的 `?` 被定义为与示例 9-5 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err``Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。
列表 9-6 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将任何 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?` 示例 9-6 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将任何 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`
`?` 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码: `?` 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码:
@ -252,11 +252,11 @@ fn read_username_from_file() -> Result<String, io::Error> {
} }
``` ```
`s` 中创建新的 `String` 被放到了函数开头;这没有什么变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open``read_to_string` 都成功没有失败时返回包含用户名 `s``Ok` 值。其功能再一次与列表 9-5 和列表 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。 `s` 中创建新的 `String` 被放到了函数开头;这没有什么变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open``read_to_string` 都成功没有失败时返回包含用户名 `s``Ok` 值。其功能再一次与示例 9-5 和示例 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。
### `?` 只能被用于返回 `Result` 的函数 ### `?` 只能被用于返回 `Result` 的函数
`?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与列表 9-5 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。 `?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与示例 9-5 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。
让我们看看在 `main` 函数中使用 `?` 会发生什么,如果你还记得的话它的返回值类型是`()` 让我们看看在 `main` 函数中使用 `?` 会发生什么,如果你还记得的话它的返回值类型是`()`

View File

@ -73,7 +73,7 @@ loop {
然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。 然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例: 相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-8 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
```rust ```rust
pub struct Guess { pub struct Guess {
@ -97,7 +97,7 @@ impl Guess {
} }
``` ```
<span class="caption">列表 9-8一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span> <span class="caption">示例 9-8一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span>
首先,我们定义了一个包含 `u32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。 首先,我们定义了一个包含 `u32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。

View File

@ -18,7 +18,7 @@
在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。 在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。
考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示: 考虑一下这个寻找列表中最大值的小程序,如示例 10-1 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -39,11 +39,11 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-1在一个数字列表中寻找最大值的函数</span> <span class="caption">示例 10-1在一个数字列表中寻找最大值的函数</span>
这段代码获取一个整型列表,存放在变量 `number_list` 中。它将列表的第一项放入了变量 `largest` 中。接着遍历了列表中的所有数字,如果当前值大于 `largest` 中储存的值,将 `largest` 替换为这个值。如果当前值小于目前为止的最大值,`largest` 保持不变。当列表中所有值都被考虑到之后,`largest` 将会是最大值,在这里也就是 100。 这段代码获取一个整型列表,存放在变量 `number_list` 中。它将列表的第一项放入了变量 `largest` 中。接着遍历了列表中的所有数字,如果当前值大于 `largest` 中储存的值,将 `largest` 替换为这个值。如果当前值小于目前为止的最大值,`largest` 保持不变。当列表中所有值都被考虑到之后,`largest` 将会是最大值,在这里也就是 100。
如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码,这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示: 如果需要在两个不同的列表中寻找最大值,我们可以重复示例 10-1 中的代码,这样程序中就会存在两段相同逻辑的代码,如示例 10-2 所示:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -75,7 +75,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-2寻找 **两个** 数字列表最大值的代码</span> <span class="caption">示例 10-2寻找 **两个** 数字列表最大值的代码</span>
虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。 虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
@ -85,7 +85,7 @@ fn main() {
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。 为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。
列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做 `largest` 的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置: 示例 10-3 的程序中将寻找最大值的代码提取到了一个叫做 `largest` 的函数中。这个程序可以找出两个不同数字列表的最大值,不过示例 10-1 中的代码只存在于一个位置:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -117,11 +117,11 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-3抽象后的寻找两个数字列表最大值的代码</span> <span class="caption">示例 10-3抽象后的寻找两个数字列表最大值的代码</span>
这个函数有一个参数 `list`,它代表会传递给函数的任何具体的 `i32`值的 slice。函数定义中的 `list` 代表任何 `&[i32]`。当调用 `largest` 函数时,其代码实际上运行于我们传递的特定值上。 这个函数有一个参数 `list`,它代表会传递给函数的任何具体的 `i32`值的 slice。函数定义中的 `list` 代表任何 `&[i32]`。当调用 `largest` 函数时,其代码实际上运行于我们传递的特定值上。
列表 10-2 到列表 10-3 中涉及的机制经历了如下几步: 示例 10-2 到示例 10-3 中涉及的机制经历了如下几步:
1. 我们注意到了重复代码。 1. 我们注意到了重复代码。
2. 我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。 2. 我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。

View File

@ -10,7 +10,7 @@
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。 定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到 `largest` 函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。第二个函数寻找 slice 中 `char` 的最大值: 回到 `largest` 函数上,示例 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从示例 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。第二个函数寻找 slice 中 `char` 的最大值:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -55,7 +55,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-4两个只在名称和签名中类型有所不同的函数</span> <span class="caption">示例 10-4两个只在名称和签名中类型有所不同的函数</span>
这里 `largest_i32``largest_char` 有着完全相同的函数体,所以如果能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现! 这里 `largest_i32``largest_char` 有着完全相同的函数体,所以如果能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!
@ -71,7 +71,7 @@ fn largest<T>(list: &[T]) -> T {
这可以理解为:函数 `largest` 有泛型类型 `T`。它有一个参数 `list`,它的类型是一个 `T` 值的 slice。`largest` 函数将会返回一个与 `T` 相同类型的值。 这可以理解为:函数 `largest` 有泛型类型 `T`。它有一个参数 `list`,它的类型是一个 `T` 值的 slice。`largest` 函数将会返回一个与 `T` 相同类型的值。
列表 10-5 展示一个在签名中使用了泛型的统一的 `largest` 函数定义,并向我们展示了如何对 `i32` 值的 slice 或 `char` 值的 slice 调用 `largest` 函数。注意这些代码还不能编译! 示例 10-5 展示一个在签名中使用了泛型的统一的 `largest` 函数定义,并向我们展示了如何对 `i32` 值的 slice 或 `char` 值的 slice 调用 `largest` 函数。注意这些代码还不能编译!
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -101,7 +101,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-5一个还不能编译的使用泛型参数的 `largest` 函数定义</span> <span class="caption">示例 10-5一个还不能编译的使用泛型参数的 `largest` 函数定义</span>
如果现在就尝试编译这些代码,会出现如下错误: 如果现在就尝试编译这些代码,会出现如下错误:
@ -126,7 +126,7 @@ what you think. /Carol -->
### 结构体定义中的泛型 ### 结构体定义中的泛型
同样也可以使用 `<>` 语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的 `x``y` 坐标值的结构体 `Point` 同样也可以使用 `<>` 语法来定义拥有一个或多个泛型参数类型字段的结构体。示例 10-6 展示了如何定义和使用一个可以存放任何类型的 `x``y` 坐标值的结构体 `Point`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -142,11 +142,11 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-6`Point` 结构体存放了两个 `T` 类型的值 `x``y`</span> <span class="caption">示例 10-6`Point` 结构体存放了两个 `T` 类型的值 `x``y`</span>
其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。 其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意 `Point` 的定义中只使用了一个泛型类型,我们想要表达的是结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x``y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像列表 10-7 中的代码就不能编译: 注意 `Point` 的定义中只使用了一个泛型类型,我们想要表达的是结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x``y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像示例 10-7 中的代码就不能编译:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -161,7 +161,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-7字段 `x``y` 必须是相同类型,因为他们都有相同的泛型类型 `T`</span> <span class="caption">示例 10-7字段 `x``y` 必须是相同类型,因为他们都有相同的泛型类型 `T`</span>
尝试编译会得到如下错误: 尝试编译会得到如下错误:
@ -179,7 +179,7 @@ error[E0308]: mismatched types
当我们将 5 赋值给 `x`,编译器就知道这个 `Point` 实例的泛型类型 `T` 是一个整型。接着我们将 `y` 指定为 4.0,而它被定义为与 `x` 有着相同的类型,所以出现了类型不匹配的错误。 当我们将 5 赋值给 `x`,编译器就知道这个 `Point` 实例的泛型类型 `T` 是一个整型。接着我们将 `y` 指定为 4.0,而它被定义为与 `x` 有着相同的类型,所以出现了类型不匹配的错误。
如果想要定义一个 `x``y` 可以有不同类型且仍然是泛型的 `Point` 结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改 `Point` 的定义为拥有两个泛型类型 `T``U`。其中字段 `x``T` 类型的,而字段 `y``U` 类型的: 如果想要定义一个 `x``y` 可以有不同类型且仍然是泛型的 `Point` 结构体,我们可以使用多个泛型类型参数。在示例 10-8 中,我们修改 `Point` 的定义为拥有两个泛型类型 `T``U`。其中字段 `x``T` 类型的,而字段 `y``U` 类型的:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -196,7 +196,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-8使用两个泛型的 `Point`,这样 `x``y` 可能是不同类型</span> <span class="caption">示例 10-8使用两个泛型的 `Point`,这样 `x``y` 可能是不同类型</span>
现在所有这些 `Point` 实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。 现在所有这些 `Point` 实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
@ -222,13 +222,13 @@ enum Result<T, E> {
} }
``` ```
`Result` 枚举有两个泛型类型,`T` 和 `E`。`Result` 有两个成员:`Ok`,它存放一个类型 `T` 的值,而 `Err` 则存放一个类型 `E` 的值。这个定义使得 `Result` 枚举能很方便的表达任何可能成功(返回 `T` 类型的值)也可能失败(返回 `E` 类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景:当文件被成功打开 `T` 被放入了 `std::fs::File` 类型而当打开文件出现问题时 `E` 被放入了 `std::io::Error` 类型。 `Result` 枚举有两个泛型类型,`T` 和 `E`。`Result` 有两个成员:`Ok`,它存放一个类型 `T` 的值,而 `Err` 则存放一个类型 `E` 的值。这个定义使得 `Result` 枚举能很方便的表达任何可能成功(返回 `T` 类型的值)也可能失败(返回 `E` 类型的值)的操作。回忆一下示例 9-2 中打开一个文件的场景:当文件被成功打开 `T` 被放入了 `std::fs::File` 类型而当打开文件出现问题时 `E` 被放入了 `std::io::Error` 类型。
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。 当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。
### 方法定义中的枚举数据类型 ### 方法定义中的枚举数据类型
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体 `Point<T>`。接着我们在 `Point<T>` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用: 可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point<T>`。接着我们在 `Point<T>` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -251,9 +251,9 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-9`Point<T>` 结构体上实现方法 `x`,它返回 `T` 类型的字段 `x` 的引用</span> <span class="caption">示例 10-9`Point<T>` 结构体上实现方法 `x`,它返回 `T` 类型的字段 `x` 的引用</span>
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的加括号中的类型是泛型而不是具体类型。例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。列表 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32` 注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的加括号中的类型是泛型而不是具体类型。例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`
```rust ```rust
# struct Point<T> { # struct Point<T> {
@ -268,11 +268,11 @@ impl Point<f32> {
} }
``` ```
<span class="caption">列表 10-10构建一个只用于拥有泛型参数 `T` 的结构体的具体类型的 `impl`</span> <span class="caption">示例 10-10构建一个只用于拥有泛型参数 `T` 的结构体的具体类型的 `impl`</span>
这段代码意味着 `Point<f32>` 类型会有一个方法 `distance_from_origin`,而其他 `T` 不是 `f32` 类型的 `Point<T>` 实例则没有定义此方法。这个方法计算点实例与另一个点坐标之间的距离,它使用了只能用于浮点型的数学运算符。 这段代码意味着 `Point<f32>` 类型会有一个方法 `distance_from_origin`,而其他 `T` 不是 `f32` 类型的 `Point<T>` 实例则没有定义此方法。这个方法计算点实例与另一个点坐标之间的距离,它使用了只能用于浮点型的数学运算符。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-11 中在列表 10-8 中的结构体 `Point<T, U>` 上定义了一个方法 `mixup`。这个方法获取另一个 `Point` 作为参数,而它可能与调用 `mixup``self` 是不同的 `Point` 类型。这个方法用 `self``Point` 类型的 `x` 值(类型 `T`)和参数的 `Point` 类型的 `y` 值(类型 `W`)来创建一个新 `Point` 类型的实例: 结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中在示例 10-8 中的结构体 `Point<T, U>` 上定义了一个方法 `mixup`。这个方法获取另一个 `Point` 作为参数,而它可能与调用 `mixup``self` 是不同的 `Point` 类型。这个方法用 `self``Point` 类型的 `x` 值(类型 `T`)和参数的 `Point` 类型的 `y` 值(类型 `W`)来创建一个新 `Point` 类型的实例:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -301,7 +301,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-11方法使用了与结构体定义中不同类型的泛型</span> <span class="caption">示例 10-11方法使用了与结构体定义中不同类型的泛型</span>
`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`
@ -313,7 +313,7 @@ fn main() {
Rust 通过在编译时进行泛型代码的 **单态化***monomorphization*)来保证效率。单态化是一个将泛型代码转变为实际放入的具体类型的特定代码的过程。 Rust 通过在编译时进行泛型代码的 **单态化***monomorphization*)来保证效率。单态化是一个将泛型代码转变为实际放入的具体类型的特定代码的过程。
编译器所做的工作正好与列表 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。 编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
让我们看看一个使用标准库中 `Option` 枚举的例子: 让我们看看一个使用标准库中 `Option` 枚举的例子:

View File

@ -14,7 +14,7 @@ trait 允许我们进行另一种抽象:他们让我们可以抽象类型所
例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。 例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle``Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summary` 方法来请求总结。列表 10-12 中展示了一个表现这个概念的 `Summarizable` trait 的定义: 我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle``Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summary` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的 `Summarizable` trait 的定义:
<span class="filename">文件名: lib.rs</span> <span class="filename">文件名: lib.rs</span>
@ -24,7 +24,7 @@ pub trait Summarizable {
} }
``` ```
<span class="caption">列表 10-12`Summarizable` trait 定义,它包含由 `summary` 方法提供的行为</span> <span class="caption">示例 10-12`Summarizable` trait 定义,它包含由 `summary` 方法提供的行为</span>
使用 `trait` 关键字来声明一个 trait后面是 trait 的名字,在这个例子中是 `Summarizable`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是是 `fn summary(&self) -> String`。在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summarizable` trait 的类型都拥有与这个签名的定义完全一致的 `summary` 方法。 使用 `trait` 关键字来声明一个 trait后面是 trait 的名字,在这个例子中是 `Summarizable`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是是 `fn summary(&self) -> String`。在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summarizable` trait 的类型都拥有与这个签名的定义完全一致的 `summary` 方法。
@ -32,7 +32,7 @@ trait 体中可以有多个方法,一行一个方法签名且都以分号结
### 为类型实现 trait ### 为类型实现 trait
现在我们定义了 `Summarizable` trait接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。列表 10-12 中展示了 `NewsArticle` 结构体上 `Summarizable` trait 的一个实现,它使用标题、作者和创建的位置作为 `summary` 的返回值。对于 `Tweet` 结构体,我们选择将 `summary` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。 现在我们定义了 `Summarizable` trait接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。示例 10-12 中展示了 `NewsArticle` 结构体上 `Summarizable` trait 的一个实现,它使用标题、作者和创建的位置作为 `summary` 的返回值。对于 `Tweet` 结构体,我们选择将 `summary` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。
<span class="filename">文件名: lib.rs</span> <span class="filename">文件名: lib.rs</span>
@ -68,7 +68,7 @@ impl Summarizable for Tweet {
} }
``` ```
<span class="caption">列表 10-13`NewsArticle``Tweet` 类型上实现 `Summarizable` trait</span> <span class="caption">示例 10-13`NewsArticle``Tweet` 类型上实现 `Summarizable` trait</span>
在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。 在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
@ -87,7 +87,7 @@ println!("1 new tweet: {}", tweet.summary());
这会打印出 `1 new tweet: horse_ebooks: of course, as you probably already know, people` 这会打印出 `1 new tweet: horse_ebooks: of course, as you probably already know, people`
注意因为列表 10-12 中我们在相同的 `lib.rs` 里定义了 `Summarizable` trait 和 `NewsArticle``Tweet` 类型,所以他们是位于同一作用域的。如果这个 `lib.rs` 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能外加为其 `WeatherForecast` 结构体实现 `Summarizable` trait在实现 `Summarizable` trait 之前他们首先就需要将其导入其作用域中,如列表 10-14 所示: 注意因为示例 10-12 中我们在相同的 `lib.rs` 里定义了 `Summarizable` trait 和 `NewsArticle``Tweet` 类型,所以他们是位于同一作用域的。如果这个 `lib.rs` 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能外加为其 `WeatherForecast` 结构体实现 `Summarizable` trait在实现 `Summarizable` trait 之前他们首先就需要将其导入其作用域中,如示例 10-14 所示:
<span class="filename">文件名: lib.rs</span> <span class="filename">文件名: lib.rs</span>
@ -111,9 +111,9 @@ impl Summarizable for WeatherForecast {
} }
``` ```
<span class="caption">列表 10-14在另一个 crate 中将 `aggregator` crate 的 `Summarizable` trait 引入作用域</span> <span class="caption">示例 10-14在另一个 crate 中将 `aggregator` crate 的 `Summarizable` trait 引入作用域</span>
另外这段代码假设 `Summarizable` 是一个公有 trait这是因为列表 10-12 中 `trait` 之前使用了 `pub` 关键字。 另外这段代码假设 `Summarizable` 是一个公有 trait这是因为示例 10-12 中 `trait` 之前使用了 `pub` 关键字。
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说不允许对外部类型实现外部 trait。例如不能在 `Vec` 上实现 `Display` trait因为 `Display``Vec` 都定义于标准库中。允许在像 `Tweet` 这样作为我们 `aggregator`crate 部分功能的自定义类型上实现标准库中的 trait `Display`。也允许在 `aggregator`crate 中为 `Vec` 实现 `Summarizable`,因为 `Summarizable` 定义与此。这个限制是我们称为 **孤儿规则***orphan rule*)的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait因而这两个实现会相互冲突Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule其他人编写的代码不会破坏你代码反之亦是如此。 trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说不允许对外部类型实现外部 trait。例如不能在 `Vec` 上实现 `Display` trait因为 `Display``Vec` 都定义于标准库中。允许在像 `Tweet` 这样作为我们 `aggregator`crate 部分功能的自定义类型上实现标准库中的 trait `Display`。也允许在 `aggregator`crate 中为 `Vec` 实现 `Summarizable`,因为 `Summarizable` 定义与此。这个限制是我们称为 **孤儿规则***orphan rule*)的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait因而这两个实现会相互冲突Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule其他人编写的代码不会破坏你代码反之亦是如此。
@ -121,7 +121,7 @@ trait 实现的一个需要注意的限制是:只能在 trait 或对应类型
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。 有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
列表 10-15 中展示了如何为 `Summarize` trait 的 `summary` 方法指定一个默认的字符串值,而不是像列表 10-12 中那样只是定义方法签名: 示例 10-15 中展示了如何为 `Summarize` trait 的 `summary` 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名:
<span class="filename">文件名: lib.rs</span> <span class="filename">文件名: lib.rs</span>
@ -133,9 +133,9 @@ pub trait Summarizable {
} }
``` ```
<span class="caption">列表 10-15`Summarizable` trait 的定义,带有一个 `summary` 方法的默认实现</span> <span class="caption">示例 10-15`Summarizable` trait 的定义,带有一个 `summary` 方法的默认实现</span>
如果想要对 `NewsArticle` 实例使用这个默认实现,而不是像列表 10-13 中那样定义一个自己的实现,则可以指定一个空的 `impl` 块: 如果想要对 `NewsArticle` 实例使用这个默认实现,而不是像示例 10-13 中那样定义一个自己的实现,则可以指定一个空的 `impl` 块:
```rust,ignore ```rust,ignore
impl Summarizable for NewsArticle {} impl Summarizable for NewsArticle {}
@ -157,7 +157,7 @@ println!("New article available! {}", article.summary());
这段代码会打印 `New article available! (Read more...)` 这段代码会打印 `New article available! (Read more...)`
`Summarizable` trait 改变为拥有默认 `summary` 实现并不要求对列表 10-13 中 `Tweet` 和列表 10-14 中 `WeatherForecast``Summarizable` 实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。 `Summarizable` trait 改变为拥有默认 `summary` 实现并不要求对示例 10-13 中 `Tweet` 和示例 10-14 中 `WeatherForecast``Summarizable` 实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。
默认实现允许调用相同 trait 中的其他方法哪怕这些方法没有默认实现。通过这种方法trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summarizable` trait 也拥有一个要求实现 的`author_summary` 方法,接着 `summary` 方法则提供默认实现并调用 `author_summary` 方法: 默认实现允许调用相同 trait 中的其他方法哪怕这些方法没有默认实现。通过这种方法trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summarizable` trait 也拥有一个要求实现 的`author_summary` 方法,接着 `summary` 方法则提供默认实现并调用 `author_summary` 方法:
@ -202,7 +202,7 @@ println!("1 new tweet: {}", tweet.summary());
现在我们定义了 trait 并在类型上实现了这些 trait也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型编译器会确保其被限制为那些实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 *trait bounds* 现在我们定义了 trait 并在类型上实现了这些 trait也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型编译器会确保其被限制为那些实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 *trait bounds*
例如在列表 10-13 中为 `NewsArticle``Tweet` 类型实现了 `Summarizable` trait。我们可以定义一个函数 `notify` 来调用 `summary` 方法,它拥有一个泛型类型 `T` 的参数 `item`。为了能够在 `item` 上调用 `summary` 而不出现错误,我们可以在 `T` 上使用 trait bounds 来指定 `item` 必须是实现了 `Summarizable` trait 的类型: 例如在示例 10-13 中为 `NewsArticle``Tweet` 类型实现了 `Summarizable` trait。我们可以定义一个函数 `notify` 来调用 `summary` 方法,它拥有一个泛型类型 `T` 的参数 `item`。为了能够在 `item` 上调用 `summary` 而不出现错误,我们可以在 `T` 上使用 trait bounds 来指定 `item` 必须是实现了 `Summarizable` trait 的类型:
```rust,ignore ```rust,ignore
pub fn notify<T: Summarizable>(item: T) { pub fn notify<T: Summarizable>(item: T) {
@ -210,7 +210,7 @@ pub fn notify<T: Summarizable>(item: T) {
} }
``` ```
trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于 `T` 上的 trait bounds我们可以传递任何 `NewsArticle``Tweet` 的实例来调用 `notify` 函数。列表 10-14 中使用我们 `aggregator` crate 的外部代码也可以传递一个 `WeatherForecast` 的实例来调用 `notify` 函数,因为 `WeatherForecast` 同样也实现了 `Summarizable`。使用任何其他类型,比如 `String``i32`,来调用 `notify` 的代码将不能编译,因为这些类型没有实现 `Summarizable` trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于 `T` 上的 trait bounds我们可以传递任何 `NewsArticle``Tweet` 的实例来调用 `notify` 函数。示例 10-14 中使用我们 `aggregator` crate 的外部代码也可以传递一个 `WeatherForecast` 的实例来调用 `notify` 函数,因为 `WeatherForecast` 同样也实现了 `Summarizable`。使用任何其他类型,比如 `String``i32`,来调用 `notify` 的代码将不能编译,因为这些类型没有实现 `Summarizable`
可以通过 `+` 来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用 `T` 类型的显示格式的同时也能使用 `summary` 方法,则可以使用 trait bounds `T: Summarizable + Display`。这意味着 `T` 可以是任何实现了 `Summarizable``Display` 的类型。 可以通过 `+` 来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用 `T` 类型的显示格式的同时也能使用 `summary` 方法,则可以使用 trait bounds `T: Summarizable + Display`。这意味着 `T` 可以是任何实现了 `Summarizable``Display` 的类型。
@ -233,7 +233,7 @@ fn some_function<T, U>(t: T, u: U) -> i32
### 使用 trait bounds 来修复 `largest` 函数 ### 使用 trait bounds 来修复 `largest` 函数
所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复列表 10-5 中那个使用泛型类型参数的 `largest` 函数定义了!当我们将其放置不管的时候,它会出现这个错误: 所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复示例 10-5 中那个使用泛型类型参数的 `largest` 函数定义了!当我们将其放置不管的时候,它会出现这个错误:
```text ```text
error[E0369]: binary operation `>` cannot be applied to type `T` error[E0369]: binary operation `>` cannot be applied to type `T`
@ -273,7 +273,7 @@ error[E0507]: cannot move out of borrowed content
错误的核心是 `cannot move out of type [T], a non-copy array`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32``char`。正如第四章讨论过的,像 `i32``char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的,这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中。 错误的核心是 `cannot move out of type [T], a non-copy array`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32``char`。正如第四章讨论过的,像 `i32``char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的,这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中。
如果只想对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`列表 10-16 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd``Copy` 这两个 trait例如 `i32``char` 如果只想对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`示例 10-16 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd``Copy` 这两个 trait例如 `i32``char`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -305,13 +305,13 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-16一个可以用于任何实现了 `PartialOrd``Copy` trait 的泛型的 `largest` 函数</span> <span class="caption">示例 10-16一个可以用于任何实现了 `PartialOrd``Copy` trait 的泛型的 `largest` 函数</span>
如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`,并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。但是使用 `clone` 函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种 `largest` 的实现方式是返回 slice 中一个 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone``Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! 如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`,并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。但是使用 `clone` 函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种 `largest` 的实现方式是返回 slice 中一个 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone``Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
### 使用 trait bound 有条件的实现方法 ### 使用 trait bound 有条件的实现方法
通过使用带有 trati bound 的泛型 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,列表 10-17 中的类型 `Pair<T>` 总是实现了 `new` 方法,不过只有 `Pair<T>` 内部的 `T` 实现了 `PartialOrd` trait 来允许比较和 `Display` trait 来启用打印,才会实现 `cmp_display` 通过使用带有 trati bound 的泛型 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,示例 10-17 中的类型 `Pair<T>` 总是实现了 `new` 方法,不过只有 `Pair<T>` 内部的 `T` 实现了 `PartialOrd` trait 来允许比较和 `Display` trait 来启用打印,才会实现 `cmp_display`
```rust ```rust
use std::fmt::Display; use std::fmt::Display;
@ -341,7 +341,7 @@ impl<T: Display + PartialOrd> Pair<T> {
} }
``` ```
<span class="caption">列表 10-17根据 trait bound 在泛型上有条件的实现方法</span> <span class="caption">示例 10-17根据 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` 块看起来像这样:

View File

@ -12,7 +12,7 @@
### 生命周期避免了悬垂引用 ### 生命周期避免了悬垂引用
生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下列表 10-18 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值: 生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下示例 10-18 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值:
```rust,ignore ```rust,ignore
{ {
@ -27,7 +27,7 @@
} }
``` ```
<span class="caption">列表 10-18尝试使用离开作用域的值的引用</span> <span class="caption">示例 10-18尝试使用离开作用域的值的引用</span>
> ### 未初始化变量不能被使用 > ### 未初始化变量不能被使用
> >
@ -51,7 +51,7 @@ error: `x` does not live long enough
#### 借用检查器 #### 借用检查器
编译器的这一部分叫做 **借用检查器***borrow checker*),它比较作用域来确保所有的借用都是有效的。列表 10-19 展示了与列表 10-18 相同的例子不过带有变量声明周期的注释: 编译器的这一部分叫做 **借用检查器***borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-19 展示了与示例 10-18 相同的例子不过带有变量声明周期的注释:
```rust,ignore ```rust,ignore
{ {
@ -68,7 +68,7 @@ error: `x` does not live long enough
} }
``` ```
<span class="caption">列表 10-19`r` 和 `x` 的生命周期注解,分别叫做 `'a``'b`</span> <span class="caption">示例 10-19`r` 和 `x` 的生命周期注解,分别叫做 `'a``'b`</span>
<!-- Just checking I'm reading this right: the inside block is the b lifetime, <!-- Just checking I'm reading this right: the inside block is the b lifetime,
correct? I want to leave a note for production, make sure we can make that correct? I want to leave a note for production, make sure we can make that
@ -80,7 +80,7 @@ looking arrows and labels? /Carol -->
我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时Rust 比较这两个生命周期的大小,并发现 `r` 拥有声明周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存活的时间更短。 我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时Rust 比较这两个生命周期的大小,并发现 `r` 拥有声明周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存活的时间更短。
让我们看看列表 10-20 中这个并没有产生悬垂引用且可以正确编译的例子: 让我们看看示例 10-20 中这个并没有产生悬垂引用且可以正确编译的例子:
```rust ```rust
{ {
@ -93,7 +93,7 @@ looking arrows and labels? /Carol -->
} // -----+ } // -----+
``` ```
<span class="caption">列表 10-20一个有效的引用因为数据比引用有着更长的生命周期</span> <span class="caption">示例 10-20一个有效的引用因为数据比引用有着更长的生命周期</span>
这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。 这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。
@ -101,7 +101,7 @@ looking arrows and labels? /Carol -->
### 函数中的泛型生命周期 ### 函数中的泛型生命周期
让我们来编写一个返回两个字符串 slice 中较长者的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了 `longest` 函数,列表 10-21 中的代码应该会打印出 `The longest string is abcd` 让我们来编写一个返回两个字符串 slice 中较长者的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-21 中的代码应该会打印出 `The longest string is abcd`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -115,7 +115,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-21`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span> <span class="caption">示例 10-21`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个</span>
注意函数期望获取字符串 slice如第四章所讲到的这是引用因为我们并不希望`longest` 函数获取其参数的所有权。我们希望函数能够接受 `String` 的 slice也就是变量 `string1` 的类型)以及字符串字面值(也就是变量 `string2` 包含的值)。 注意函数期望获取字符串 slice如第四章所讲到的这是引用因为我们并不希望`longest` 函数获取其参数的所有权。我们希望函数能够接受 `String` 的 slice也就是变量 `string1` 的类型)以及字符串字面值(也就是变量 `string2` 包含的值)。
@ -138,7 +138,7 @@ interested to know if rereading Chapter 4 clears up that confusion.
参考之前第四章中的 “字符串 slice 作为参数” 部分中更多关于为什么上面例子中的参数正符合我们期望的讨论。 参考之前第四章中的 “字符串 slice 作为参数” 部分中更多关于为什么上面例子中的参数正符合我们期望的讨论。
如果尝试像列表 10-22 中那样实现 `longest` 函数,它并不能编译: 如果尝试像示例 10-22 中那样实现 `longest` 函数,它并不能编译:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -152,7 +152,7 @@ fn longest(x: &str, y: &str) -> &str {
} }
``` ```
<span class="caption">列表 10-22一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译</span> <span class="caption">示例 10-22一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译</span>
将会出现如下有关生命周期的错误: 将会出现如下有关生命周期的错误:
@ -168,7 +168,7 @@ error[E0106]: missing lifetime specifier
提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x``y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用。 提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x``y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用。
虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像列表 10-19 和 10-20 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x``y` 的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。 虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-19 和 10-20 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x``y` 的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
### 生命周期注解语法 ### 生命周期注解语法
@ -188,7 +188,7 @@ error[E0106]: missing lifetime specifier
### 函数签名中的生命周期注解 ### 函数签名中的生命周期注解
来看看我们编写的 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-23 中在每个引用中都加上了 `'a` 那样: 来看看我们编写的 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-23 中在每个引用中都加上了 `'a` 那样:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -202,9 +202,9 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
} }
``` ```
<span class="caption">列表 10-23`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a`</span> <span class="caption">示例 10-23`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a`</span>
这段代码能够编译并会产生我们想要使用列表 10-21 中的 `main` 函数得到的结果。 这段代码能够编译并会产生我们想要使用示例 10-21 中的 `main` 函数得到的结果。
现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。 现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。
@ -214,7 +214,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x``y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在 `x``y` 中较短的那个生命周期结束之前保持有效。 当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x``y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在 `x``y` 中较短的那个生命周期结束之前保持有效。
让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制 `longest` 函数的使用的。列表 10-24 是一个应该在任何编程语言中都很直观的例子:`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long` 让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制 `longest` 函数的使用的。示例 10-24 是一个应该在任何编程语言中都很直观的例子:`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -238,9 +238,9 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-24通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数</span> <span class="caption">示例 10-24通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数</span>
接下来,让我们尝试一个 `result` 的引用的生命周期必须比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,不过将 `result``string2` 变量的赋值语句一同放在内部作用域里。接下来,我们将使用 `result``println!` 移动到内部作用域之外,就在其结束之后。注意列表 10-25 中的代码不能编译: 接下来,让我们尝试一个 `result` 的引用的生命周期必须比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,不过将 `result``string2` 变量的赋值语句一同放在内部作用域里。接下来,我们将使用 `result``println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-25 中的代码不能编译:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -256,7 +256,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-25`string2` 离开作用域之后使用 `result` 的尝试不能编译</span> <span class="caption">示例 10-25`string2` 离开作用域之后使用 `result` 的尝试不能编译</span>
如果尝试编译会出现如下错误: 如果尝试编译会出现如下错误:
@ -274,7 +274,7 @@ error: `string2` does not live long enough
错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a` 错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`
以我们的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许列表 10-25 中的代码,因为它可能会存在无效的引用。 以我们的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-25 中的代码,因为它可能会存在无效的引用。
请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确! 请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确!
@ -326,7 +326,7 @@ at 1:44...
### 结构体定义中的生命周期注解 ### 结构体定义中的生命周期注解
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-26 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt` 目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。示例 10-26 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -344,7 +344,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 10-26一个存放引用的结构体所以其定义需要生命周期注解</span> <span class="caption">示例 10-26一个存放引用的结构体所以其定义需要生命周期注解</span>
这个结构体有一个字段,`part`,它存放了一个字符串 slice这是一个引用。类似于泛型参数类型必须在结构体名称后面的尖括号中声明泛型生命周期参数以便在结构体定义中使用生命周期参数。 这个结构体有一个字段,`part`,它存放了一个字符串 slice这是一个引用。类似于泛型参数类型必须在结构体名称后面的尖括号中声明泛型生命周期参数以便在结构体定义中使用生命周期参数。
@ -352,7 +352,7 @@ fn main() {
### 生命周期省略 ### 生命周期省略
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的 “字符串 slice” 部分有一个函数,我们在列表 10-27 中再次展示出来,它没有生命周期注解却能成功编译: 在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的 “字符串 slice” 部分有一个函数,我们在示例 10-27 中再次展示出来,它没有生命周期注解却能成功编译:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -370,7 +370,7 @@ fn first_word(s: &str) -> &str {
} }
``` ```
<span class="caption">列表 10-27第四章定义了一个没有使用生命周期注解的函数即便其参数和返回值都是引用</span> <span class="caption">示例 10-27第四章定义了一个没有使用生命周期注解的函数即便其参数和返回值都是引用</span>
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: 这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
@ -396,7 +396,7 @@ fn first_word<'a>(s: &'a str) -> &'a str {
3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self``&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。 3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self``&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法写起来更简洁。
假设我们自己就是编译器并来计算列表 10-25 `first_word` 函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: 假设我们自己就是编译器并来计算示例 10-25 `first_word` 函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:
```rust,ignore ```rust,ignore
fn first_word(s: &str) -> &str { fn first_word(s: &str) -> &str {
@ -416,7 +416,7 @@ fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,而编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。 现在这个函数签名中的所有引用都有了生命周期,而编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
让我们再看看另一个例子,这次我们从列表 10-22 中没有生命周期参数的 `longest` 函数开始: 让我们再看看另一个例子,这次我们从示例 10-22 中没有生命周期参数的 `longest` 函数开始:
```rust,ignore ```rust,ignore
fn longest(x: &str, y: &str) -> &str { fn longest(x: &str, y: &str) -> &str {
@ -428,7 +428,7 @@ fn longest(x: &str, y: &str) -> &str {
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
``` ```
再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译列表 10-22 的代码时会出现错误的原因:编译器适用所有已知的生命周期省略规则,不过仍然不能计算出签名中所有引用的生命周期。 再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-22 的代码时会出现错误的原因:编译器适用所有已知的生命周期省略规则,不过仍然不能计算出签名中所有引用的生命周期。
因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。 因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。
@ -442,11 +442,11 @@ parameters need to be declared and used since the lifetime parameters could go
with the struct's fields or with references passed into or returned from with the struct's fields or with references passed into or returned from
methods. /Carol --> methods. /Carol -->
当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-11 中展示的泛型类型参数的语法:声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关 当为带有生命周期的结构体实现方法时,其语法依然类似示例 10-11 中展示的泛型类型参数的语法:声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关
(实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。 (实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-26 中定义的结构体 `ImportantExcerpt` 的例子。 `impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-26 中定义的结构体 `ImportantExcerpt` 的例子。
首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值: 首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值:
@ -515,7 +515,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st
} }
``` ```
这个是列表 10-23 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 这个是示例 10-23 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。
## 总结 ## 总结

View File

@ -35,13 +35,13 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-1`cargo new` 自动生成的测试模块和函数</span> <span class="caption">示例 11-1`cargo new` 自动生成的测试模块和函数</span>
现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 注解并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。 现在让我们暂时忽略 `tests` 模块和 `#[cfg(test)]` 注解并只关注函数来了解其如何工作。注意 `fn` 行之前的 `#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在 `tests` 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 `#[test]` 属性标明哪些函数是测试。
这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。 这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。
`cargo test` 命令会运行项目中所有的测试,如列表 11-2 所示: `cargo test` 命令会运行项目中所有的测试,如示例 11-2 所示:
```text ```text
$ cargo test $ cargo test
@ -61,7 +61,7 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
``` ```
<span class="caption">列表 11-2运行自动生成测试的输出</span> <span class="caption">示例 11-2运行自动生成测试的输出</span>
Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这几行之后,可以看到 `running 1 test` 这一行。下一行显示了生成的测试函数的名称,它是 `it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.` 意味着所有测试都通过了。`1 passed; 0 failed` 表示通过或失败的测试数量。 Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这几行之后,可以看到 `running 1 test` 这一行。下一行显示了生成的测试函数的名称,它是 `it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.` 意味着所有测试都通过了。`1 passed; 0 failed` 表示通过或失败的测试数量。
@ -91,7 +91,7 @@ test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
``` ```
让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏!写入新函数后 `src/lib.rs` 现在看起来如列表 11-3 所示: 让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 `panic!` 宏!写入新函数后 `src/lib.rs` 现在看起来如示例 11-3 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -109,10 +109,10 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-3增加第二个测试他会失败因为调用了 `panic!`</span> <span class="caption">示例 11-3增加第二个测试他会失败因为调用了 `panic!`</span>
再次 `cargo test` 运行测试。输出应该看起来像列表 11-4它表明 `exploration` 测试通过了而 `another` 失败了: 再次 `cargo test` 运行测试。输出应该看起来像示例 11-4它表明 `exploration` 测试通过了而 `another` 失败了:
```text ```text
running 2 tests running 2 tests
@ -133,7 +133,7 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed error: test failed
``` ```
<span class="caption">列表 11-4一个测试通过和一个测试失败的测试结果</span> <span class="caption">示例 11-4一个测试通过和一个测试失败的测试结果</span>
`test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。 `test tests::another` 这一行是 `FAILED` 而不是 `ok` 了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another` 因为 `panicked at 'Make this test fail'` 而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。
@ -145,7 +145,7 @@ error: test failed
`assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个计算为布尔值的参数。如果值是 `true``assert!` 什么也不做同时测试会通过。如果值为 `false``assert!` 调用 `panic!` 宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。 `assert!` 宏由标准库提供,在希望确保测试中一些条件为 `true` 时非常有用。需要向 `assert!` 宏提供一个计算为布尔值的参数。如果值是 `true``assert!` 什么也不做同时测试会通过。如果值为 `false``assert!` 调用 `panic!` 宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。
回忆一下第五章中,列表 5-9 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在列表 11-5 中再次使用他们。将他们放进 *src/lib.rs* 而不是 *src/main.rs* 并使用 `assert!` 宏编写一些测试。 回忆一下第五章中,示例 5-9 中有一个 `Rectangle` 结构体和一个 `can_hold` 方法,在示例 11-5 中再次使用他们。将他们放进 *src/lib.rs* 而不是 *src/main.rs* 并使用 `assert!` 宏编写一些测试。
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -163,9 +163,9 @@ impl Rectangle {
} }
``` ```
<span class="caption">列表 11-5第五章中 `Rectangle` 结构体和其 `can_hold` 方法</span> <span class="caption">示例 11-5第五章中 `Rectangle` 结构体和其 `can_hold` 方法</span>
`can_hold` 方法返回一个布尔值,这意味着它完美符合 `assert!` 宏的使用场景。在列表 11-6 中,让我们编写一个 `can_hold` 方法的测试来作为练习,这里创建一个长为 8 宽为 7 的 `Rectangle` 实例,并假设它可以放得下另一个长为 5 宽为 1 的 `Rectangle` 实例: `can_hold` 方法返回一个布尔值,这意味着它完美符合 `assert!` 宏的使用场景。在示例 11-6 中,让我们编写一个 `can_hold` 方法的测试来作为练习,这里创建一个长为 8 宽为 7 的 `Rectangle` 实例,并假设它可以放得下另一个长为 5 宽为 1 的 `Rectangle` 实例:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -184,7 +184,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-6一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span> <span class="caption">示例 11-6一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形</span>
注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在 `tests` 模块中都是可用的。 注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在 `tests` 模块中都是可用的。
@ -276,7 +276,7 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `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!` 宏测试这个函数:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -296,7 +296,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-7使用 `assert_eq!` 宏测试 `add_two`</span> <span class="caption">示例 11-7使用 `assert_eq!` 宏测试 `add_two`</span>
测试通过了! 测试通过了!
@ -423,11 +423,11 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
### 使用 `should_panic` 检查 panic ### 使用 `should_panic` 检查 panic
除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的 `Guess` 类型。其他使用 `Guess` 的代码依赖于 `Guess` 实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。 除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章示例 9-8 创建的 `Guess` 类型。其他使用 `Guess` 的代码依赖于 `Guess` 实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。
可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。 可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
列表 11-8 展示了如何编写一个测试来检查 `Guess::new` 按照我们的期望出现的错误情况: 示例 11-8 展示了如何编写一个测试来检查 `Guess::new` 按照我们的期望出现的错误情况:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -460,7 +460,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-8测试会造成 `panic!` 的条件</span> <span class="caption">示例 11-8测试会造成 `panic!` 的条件</span>
`#[should_panic]` 属性位于 `#[test]` 之后和对应的测试函数之前。让我们看看测试通过时它时什么样子: `#[should_panic]` 属性位于 `#[test]` 之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:
@ -491,7 +491,7 @@ impl Guess {
} }
``` ```
如果运行列表 11-8 的测试,它会失败: 如果运行示例 11-8 的测试,它会失败:
```text ```text
running 1 test running 1 test
@ -507,7 +507,7 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 `#[should_panic]`。这个错误意味着代码中函数 `Guess::new(200)` 并没有产生 panic。 这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 `#[should_panic]`。这个错误意味着代码中函数 `Guess::new(200)` 并没有产生 panic。
然而 `should_panic` 测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。`should_panic` 甚至在测试因为其他不同的原因而不是我们期望发生的情况而 panic 时也会通过。为了使 `should_panic` 测试更精确,可以给 `should_panic` 属性增加一个可选的 `expected` 参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的 `Guess`,这里 `new` 函数根据其值是过大还或者过小而提供不同的 panic 信息: 然而 `should_panic` 测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。`should_panic` 甚至在测试因为其他不同的原因而不是我们期望发生的情况而 panic 时也会通过。为了使 `should_panic` 测试更精确,可以给 `should_panic` 属性增加一个可选的 `expected` 参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑示例 11-9 中修改过的 `Guess`,这里 `new` 函数根据其值是过大还或者过小而提供不同的 panic 信息:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -544,7 +544,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-9一个会带有特定错误信息的 `panic!` 条件的测试</span> <span class="caption">示例 11-9一个会带有特定错误信息的 `panic!` 条件的测试</span>
这个测试会通过,因为 `should_panic` 属性中 `expected` 参数提供的值是 `Guess::new` 函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是 `Guess value must be less than or equal to 100, got 200.`。这依赖于 panic 有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 `else if value > 100` 的情况下运行。 这个测试会通过,因为 `should_panic` 属性中 `expected` 参数提供的值是 `Guess::new` 函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是 `Guess value must be less than or equal to 100, got 200.`。这依赖于 panic 有多独特或动态,和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 `else if value > 100` 的情况下运行。

View File

@ -26,7 +26,7 @@ $ cargo test -- --test-threads=1
如果测试通过了Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用 `println!` 而测试通过了,我们将不会在终端看到 `println!` 的输出:只会看到说明测试通过的行。如果测试失败了,就会看到所有标准输出和其他错误信息。 如果测试通过了Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用 `println!` 而测试通过了,我们将不会在终端看到 `println!` 的输出:只会看到说明测试通过的行。如果测试失败了,就会看到所有标准输出和其他错误信息。
例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试 例如,示例 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -54,7 +54,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-10一个调用了 `println!` 的函数的测试</span> <span class="caption">示例 11-10一个调用了 `println!` 的函数的测试</span>
运行 `cargo test` 将会看到这些测试的输出: 运行 `cargo test` 将会看到这些测试的输出:
@ -85,7 +85,7 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
$ cargo test -- --nocapture $ cargo test -- --nocapture
``` ```
使用 `--nocapture` 参数再次运行列表 11-10 中的测试会显示: 使用 `--nocapture` 参数再次运行示例 11-10 中的测试会显示:
```text ```text
running 2 tests running 2 tests
@ -111,7 +111,7 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。可以向 `cargo test` 传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。 有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。可以向 `cargo test` 传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
为了展示如何运行测试的子集,列表 11-11 使用 `add_two` 函数创建了三个测试来供我们选择运行哪一个: 为了展示如何运行测试的子集,示例 11-11 使用 `add_two` 函数创建了三个测试来供我们选择运行哪一个:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -141,7 +141,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-11不同名称的三个测试</span> <span class="caption">示例 11-11不同名称的三个测试</span>
如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行: 如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:

View File

@ -33,7 +33,7 @@ mod tests {
#### 测试私有函数 #### 测试私有函数
测试社区中一直存在关于是否应该对私有函数进行单元测试的论战而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数 `internal_adder` 的代码: 测试社区中一直存在关于是否应该对私有函数进行单元测试的论战而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑示例 11-12 中带有私有函数 `internal_adder` 的代码:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -57,7 +57,7 @@ mod tests {
} }
``` ```
<span class="caption">列表 11-12测试私有函数</span> <span class="caption">示例 11-12测试私有函数</span>
注意 `internal_adder` 函数并没有标记为 `pub`,不过因为测试也不过是 Rust 代码同时 `tests` 也仅仅是另一个模块,我们完全可以在测试中导入和调用 `internal_adder`。如果你并不认为私有函数应该被测试Rust 也不会强迫你这么做。 注意 `internal_adder` 函数并没有标记为 `pub`,不过因为测试也不过是 Rust 代码同时 `tests` 也仅仅是另一个模块,我们完全可以在测试中导入和调用 `internal_adder`。如果你并不认为私有函数应该被测试Rust 也不会强迫你这么做。
@ -69,7 +69,7 @@ mod tests {
为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件Cargo 会将每一个文件当作单独的 crate 来编译。 为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件Cargo 会将每一个文件当作单独的 crate 来编译。
让我们试一试吧!保留列表 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入列表 11-13 中的代码。 让我们试一试吧!保留示例 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入示例 11-13 中的代码。
<span class="filename">文件名: tests/integration_test.rs</span> <span class="filename">文件名: tests/integration_test.rs</span>
@ -82,7 +82,7 @@ fn it_adds_two() {
} }
``` ```
<span class="caption">列表 11-13一个 `adder` crate 中函数的集成测试</span> <span class="caption">示例 11-13一个 `adder` crate 中函数的集成测试</span>
我们在顶部增加了 `extern crate adder`,这在单元测试中是不需要的。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate所以需要在每一个文件中导入库。集成测试就像其他库使用者那样通过导入 crate 并只使用公有 API。 我们在顶部增加了 `extern crate adder`,这在单元测试中是不需要的。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate所以需要在每一个文件中导入库。集成测试就像其他库使用者那样通过导入 crate 并只使用公有 API。
@ -113,7 +113,7 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
``` ```
现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做 `internal` 的测试),接着是一个单元测试的总结行。 现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(示例 11-12 中有一个叫做 `internal` 的测试),接着是一个单元测试的总结行。
集成测试部分以行 `Running target/debug/deps/integration-test-ce99bcc2479f4607`(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在 `Doc-tests adder` 部分开始之前的集成测试的总结行。 集成测试部分以行 `Running target/debug/deps/integration-test-ce99bcc2479f4607`(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在 `Doc-tests adder` 部分开始之前的集成测试的总结行。

View File

@ -24,7 +24,7 @@ $ cargo run searchstring example-filename.txt
首先我们需要程序能够获取传递给它的命令行参数的值,为此需要一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器***iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个 vector比如包含所有迭代器产生元素的 vector。 首先我们需要程序能够获取传递给它的命令行参数的值,为此需要一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器***iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个 vector比如包含所有迭代器产生元素的 vector。
让我们尝试一下:使用列表 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。 让我们尝试一下:使用示例 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -37,7 +37,7 @@ fn main() {
} }
``` ```
列表 12-1将命令行参数收集到一个 vector 中并打印出来 示例 12-1将命令行参数收集到一个 vector 中并打印出来
首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些;这样容易被错认成一个定义于当前模块的函数。 首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些;这样容易被错认成一个定义于当前模块的函数。
@ -62,7 +62,7 @@ $ cargo run needle haystack
### 将参数值保存进变量 ### 将参数值保存进变量
打印出参数 vector 中的值展示了程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值。让我们如列表 12-2 这样做: 打印出参数 vector 中的值展示了程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值。让我们如示例 12-2 这样做:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -80,7 +80,7 @@ fn main() {
} }
``` ```
列表 12-2创建变量来存放查询参数和文件名参数 示例 12-2创建变量来存放查询参数和文件名参数
正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 `args[0]`,所以我们从索引 `1` 开始。`minigrep` 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 `query` 中。第二个参数将是文件名,所以将第二个参数的引用放入变量 `filename` 中。 正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 `args[0]`,所以我们从索引 `1` 开始。`minigrep` 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 `query` 中。第二个参数将是文件名,所以将第二个参数的引用放入变量 `filename` 中。

View File

@ -4,7 +4,7 @@
> <br> > <br>
> commit b693c8400817f1022820fd63e3529cbecc35070c > commit b693c8400817f1022820fd63e3529cbecc35070c
接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森Emily Dickinson的诗它正适合这个工作在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?" 接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森Emily Dickinson的诗它正适合这个工作在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?"
<span class="filename">文件名: poem.txt</span> <span class="filename">文件名: poem.txt</span>
@ -20,9 +20,9 @@ To tell your name the livelong day
To an admiring bog! To an admiring bog!
``` ```
<span class="caption">列表 12-3艾米莉·狄金森的诗 “Im nobody! Who are you?”,一个好的测试用例</span> <span class="caption">示例 12-3艾米莉·狄金森的诗 “Im nobody! Who are you?”,一个好的测试用例</span>
创建完这个文件之后,修改 *src/main.rs* 并增加如列表 12-4 所示的打开文件的代码: 创建完这个文件之后,修改 *src/main.rs* 并增加如示例 12-4 所示的打开文件的代码:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -51,7 +51,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 12-4读取第二个参数所指定的文件内容</span> <span class="caption">示例 12-4读取第二个参数所指定的文件内容</span>
首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs::File` 来处理文件,而 `std::io::prelude::*` 则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io` 也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude必须显式 `use` 位于 `std::io` 中的 prelude。 首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs::File` 来处理文件,而 `std::io::prelude::*` 则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io` 也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude必须显式 `use` 位于 `std::io` 中的 prelude。

View File

@ -33,7 +33,7 @@
### 提取参数解析器 ### 提取参数解析器
首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 做准备。列表 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中: 首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 做准备。示例 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -54,7 +54,7 @@ fn parse_config(args: &[String]) -> (&str, &str) {
} }
``` ```
<span class="caption">列表 12-5`main` 中提取出 `parse_config` 函数</span> <span class="caption">示例 12-5`main` 中提取出 `parse_config` 函数</span>
我们仍然将命令行参数收集进一个 vector不过不同于在`main`函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `filename`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。仍然在 `main` 中创建变量 `query``filename`,不过 `main` 不再负责处理命令行参数与变量如何对应。 我们仍然将命令行参数收集进一个 vector不过不同于在`main`函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `filename`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。仍然在 `main` 中创建变量 `query``filename`,不过 `main` 不再负责处理命令行参数与变量如何对应。
@ -68,7 +68,7 @@ fn parse_config(args: &[String]) -> (&str, &str) {
> 注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为 **基本类型偏执***primitive obsession*)。 > 注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为 **基本类型偏执***primitive obsession*)。
列表 12-6 展示了新定义的结构体 `Config`,它有字段 `query``filename`。我们也改变了 `parse_config` 函数来返回一个 `Config` 结构体的实例,并更新 `main` 来使用结构体字段而不是单独的变量: 示例 12-6 展示了新定义的结构体 `Config`,它有字段 `query``filename`。我们也改变了 `parse_config` 函数来返回一个 `Config` 结构体的实例,并更新 `main` 来使用结构体字段而不是单独的变量:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -102,7 +102,7 @@ fn parse_config(args: &[String]) -> Config {
} }
``` ```
<span class="caption">列表 12-6重构 `parse_config` 返回一个 `Config` 结构体实例</span> <span class="caption">示例 12-6重构 `parse_config` 返回一个 `Config` 结构体实例</span>
`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 的借用规则。
@ -120,7 +120,7 @@ fn parse_config(args: &[String]) -> Config {
目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query``filename` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query``filename` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。 目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query``filename` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query``filename` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。
所以现在 `parse_config` 函数的目的是创建一个 `Config` 实例,我们可以将 `parse_config` 从一个普通函数变为一个叫做 `new` 的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的 `String` 调用 `String::new` 来创建一个该类型的实例那样,将 `parse_config` 变为一个与 `Config` 关联的 `new` 函数。列表 12-7 展示了需要做出的修改: 所以现在 `parse_config` 函数的目的是创建一个 `Config` 实例,我们可以将 `parse_config` 从一个普通函数变为一个叫做 `new` 的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的 `String` 调用 `String::new` 来创建一个该类型的实例那样,将 `parse_config` 变为一个与 `Config` 关联的 `new` 函数。示例 12-7 展示了需要做出的修改:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -153,7 +153,7 @@ impl Config {
} }
``` ```
<span class="caption">列表 12-7`parse_config` 变为 `Config::new`</span> <span class="caption">示例 12-7`parse_config` 变为 `Config::new`</span>
这里将 `main` 中调用 `parse_config` 的地方更新为调用 `Config::new`。我们将 `parse_config` 的名字改为 `new` 并将其移动到 `impl` 块中,这使得 `new` 函数与 `Config` 相关联。再次尝试编译并确保它可以工作。 这里将 `main` 中调用 `parse_config` 的地方更新为调用 `Config::new`。我们将 `parse_config` 的名字改为 `new` 并将其移动到 `impl` 块中,这使得 `new` 函数与 `Config` 相关联。再次尝试编译并确保它可以工作。
@ -174,7 +174,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
### 改善错误信息 ### 改善错误信息
列表 12-8 中,在 `new` 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息: 示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -187,9 +187,9 @@ fn new(args: &[String]) -> Config {
// ...snip... // ...snip...
``` ```
<span class="caption">列表 12-8增加一个参数数量检查</span> <span class="caption">示例 12-8增加一个参数数量检查</span>
这类似于列表 9-8 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 这类似于示例 9-8 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果
`args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。
有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么: 有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
@ -202,13 +202,13 @@ thread 'main' panicked at 'not enough arguments', src/main.rs:29
note: Run with `RUST_BACKTRACE=1` for a backtrace. note: Run with `RUST_BACKTRACE=1` for a backtrace.
``` ```
这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用列表 9-8 中的技术可能不是最好的;无论如何 `panic!` 调用更适合程序上的问题而不是使用上的问题,正如第九章所讲到的。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的 `Result` 这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用示例 9-8 中的技术可能不是最好的;无论如何 `panic!` 调用更适合程序上的问题而不是使用上的问题,正如第九章所讲到的。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的 `Result`
#### 从 `new` 中返回 `Result` 而不是调用 `panic!` #### 从 `new` 中返回 `Result` 而不是调用 `panic!`
我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。当 `Config::new``main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main``Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'``RUST_BACKTRACE` 的文本。 我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。当 `Config::new``main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main``Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'``RUST_BACKTRACE` 的文本。
列表 12-9 展示了为了返回 `Result``Config::new` 的返回值和函数体中所需的改变: 示例 12-9 展示了为了返回 `Result``Config::new` 的返回值和函数体中所需的改变:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -227,7 +227,7 @@ impl Config {
} }
``` ```
<span class="caption">列表 12-9`Config::new` 中返回 `Result`</span> <span class="caption">示例 12-9`Config::new` 中返回 `Result`</span>
现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是一个字符串字面值,也是目前的错误信息。 现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是一个字符串字面值,也是目前的错误信息。
@ -237,7 +237,7 @@ impl Config {
### `Config::new` 调用并处理错误 ### `Config::new` 调用并处理错误
为了处理错误情况并打印一个对用户友好的信息,我们需要像列表 12-10 那样更新 `main` 函数来处理现在 `Config::new` 返回的 `Result`。另外还需要负责手动实现 `panic!` 的使用非零错误码退出命令行工具的工作。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。 为了处理错误情况并打印一个对用户友好的信息,我们需要像示例 12-10 那样更新 `main` 函数来处理现在 `Config::new` 返回的 `Result`。另外还需要负责手动实现 `panic!` 的使用非零错误码退出命令行工具的工作。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -255,11 +255,11 @@ fn main() {
// ...snip... // ...snip...
``` ```
<span class="caption">列表 12-10如果新建 `Config` 失败则使用错误码退出</span> <span class="caption">示例 12-10如果新建 `Config` 失败则使用错误码退出</span>
在上面的列表中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的 `Result<T, E>` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result``Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包***closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是列表 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。 在上面的示例中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的 `Result<T, E>` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result``Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包***closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。
我们新增了一个 `use` 行来从标准库中导入 `process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 `err` 值,接着调用了 `std::process::exit`。`process::exit` 会立即停止程序并将传递给它的数字作为退出状态码。这类似于列表 12-8 中使用的基于 `panic!` 的错误处理,除了不会再得到所有的额外输出了。让我们试试: 我们新增了一个 `use` 行来从标准库中导入 `process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 `err` 值,接着调用了 `std::process::exit`。`process::exit` 会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 `panic!` 的错误处理,除了不会再得到所有的额外输出了。让我们试试:
```text ```text
$ cargo run $ cargo run
@ -275,7 +275,7 @@ Problem parsing arguments: not enough arguments
现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如 “二进制项目的关注分离” 部分所展开的讨论,我们将提取一个叫做 `run` 的函数来存放目前 `main `函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main` 函数将简明的足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。 现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如 “二进制项目的关注分离” 部分所展开的讨论,我们将提取一个叫做 `run` 的函数来存放目前 `main `函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main` 函数将简明的足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。
列表 12-11 展示了提取出来的 `run` 函数。目前我们只进行小的增量式的提取函数的改进。我们仍将在 *src/main.rs* 中定义这个函数: 示例 12-11 展示了提取出来的 `run` 函数。目前我们只进行小的增量式的提取函数的改进。我们仍将在 *src/main.rs* 中定义这个函数:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -302,13 +302,13 @@ fn run(config: Config) {
// ...snip... // ...snip...
``` ```
<span class="caption">列表 12-11提取 `run` 函数来包含剩余的程序逻辑</span> <span class="caption">示例 12-11提取 `run` 函数来包含剩余的程序逻辑</span>
现在 `run` 函数包含了 `main` 中从读取文件开始的剩余的所有逻辑。`run` 函数获取一个 `Config` 实例作为参数。 现在 `run` 函数包含了 `main` 中从读取文件开始的剩余的所有逻辑。`run` 函数获取一个 `Config` 实例作为参数。
#### 从 `run` 函数中返回错误 #### 从 `run` 函数中返回错误
通过将剩余的逻辑分离进 `run` 函数而不是留在 `main` 中,就可以像列表 12-9 中的 `Config::new` 那样改进错误处理。不再通过 `expect` 允许程序 panic`run` 函数将会在出错时返回一个 `Result<T, E>`。这让我们进一步以一种对用户友好的方式统一 `main` 中的错误处理。列表 12-12 展示了 `run` 签名和函数体中的改变: 通过将剩余的逻辑分离进 `run` 函数而不是留在 `main` 中,就可以像示例 12-9 中的 `Config::new` 那样改进错误处理。不再通过 `expect` 允许程序 panic`run` 函数将会在出错时返回一个 `Result<T, E>`。这让我们进一步以一种对用户友好的方式统一 `main` 中的错误处理。示例 12-12 展示了 `run` 签名和函数体中的改变:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -329,7 +329,7 @@ fn run(config: Config) -> Result<(), Box<Error>> {
} }
``` ```
<span class="caption">列表 12-12修改 `run` 函数返回 `Result`</span> <span class="caption">示例 12-12修改 `run` 函数返回 `Result`</span>
`Result<(), Box<Error>>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 `Result<(), Box<Error>>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。
对于错误类型,使用了 **trait 对象** `Box<Error>`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box<Error>` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。 对于错误类型,使用了 **trait 对象** `Box<Error>`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box<Error>` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。
@ -353,7 +353,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在
#### 处理 `main``run` 返回的错误 #### 处理 `main``run` 返回的错误
我们将检查错误并使用类似列表 12-10 中 `Config::new` 处理错误的技术来处理他们,不过有一些细微的不同: 我们将检查错误并使用类似示例 12-10 中 `Config::new` 处理错误的技术来处理他们,不过有一些细微的不同:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -387,7 +387,7 @@ fn main() {
- `Config` 的定义 - `Config` 的定义
- `Config::new` 函数定义 - `Config::new` 函数定义
现在 *src/lib.rs* 的内容应该看起来像列表 12-13为了简洁省略了函数体 现在 *src/lib.rs* 的内容应该看起来像示例 12-13为了简洁省略了函数体
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -412,11 +412,11 @@ pub fn run(config: Config) -> Result<(), Box<Error>> {
} }
``` ```
<span class="caption">列表 12-13`Config``run` 移动到 *src/lib.rs*</span> <span class="caption">示例 12-13`Config``run` 移动到 *src/lib.rs*</span>
这里使用了公有的 `pub`:在 `Config`、其字段和其 `new`方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 这里使用了公有的 `pub`:在 `Config`、其字段和其 `new`方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
现在需要在 *src/main.rs* 中使用 `extern crate greprs` 将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域。接着我们将增加一个 `use greprs::Config` 行将 `Config` 类型引入作用域,并使用库 crate 的名称作为 `run` 函数的前缀,如列表 12-14 所示: 现在需要在 *src/main.rs* 中使用 `extern crate greprs` 将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域。接着我们将增加一个 `use greprs::Config` 行将 `Config` 类型引入作用域,并使用库 crate 的名称作为 `run` 函数的前缀,如示例 12-14 所示:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -436,7 +436,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 12-14`minigrep` crate 引入 *src/main.rs* 的作用域</span> <span class="caption">示例 12-14`minigrep` crate 引入 *src/main.rs* 的作用域</span>
为了将库 crate 引入二进制 crate我们使用 `extern crate minigrep`。接着增加 `use minigrep::Config``Config` 类型引入作用域,并使用 crate 名作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。 为了将库 crate 引入二进制 crate我们使用 `extern crate minigrep`。接着增加 `use minigrep::Config``Config` 类型引入作用域,并使用 crate 名作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。

View File

@ -15,11 +15,11 @@
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。 这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行列表的功能。我们将在一个叫做 `search` 的函数中增加这些功能。 我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行示例的功能。我们将在一个叫做 `search` 的函数中增加这些功能。
### 编写失败测试 ### 编写失败测试
首先,去掉 *src/lib.rs**src/main.rs* 中的`println!`语句,因为不再真正需要他们了。接着我们会像第十一章那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。列表 12-15 展示了这个测试: 首先,去掉 *src/lib.rs**src/main.rs* 中的`println!`语句,因为不再真正需要他们了。接着我们会像第十一章那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -48,11 +48,11 @@ Pick three.";
} }
``` ```
<span class="caption">列表 12-15创建一个我们期望的 `search` 函数的失败测试</span> <span class="caption">示例 12-15创建一个我们期望的 `search` 函数的失败测试</span>
这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言 `search` 函数的返回值只包含期望的那一行。 这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言 `search` 函数的返回值只包含期望的那一行。
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -62,7 +62,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">列表 12-16刚好足够使测试通过编译的 `search` 函数定义</span> <span class="caption">示例 12-16刚好足够使测试通过编译的 `search` 函数定义</span>
注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下第十章中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query` slice 的字符串 slice。 注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下第十章中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query` slice 的字符串 slice。
@ -120,15 +120,15 @@ error: test failed
* 遍历每一行文本。 * 遍历每一行文本。
* 查看这一行是否包含要搜索的字符串。 * 查看这一行是否包含要搜索的字符串。
* 如果有,将这一行加入返回列表中。 * 如果有,将这一行加入返回示例中。
* 如果没有,什么也不做。 * 如果没有,什么也不做。
* 返回匹配到的列表 * 返回匹配到的示例
让我们一步一步的来,从遍历每行开始。 让我们一步一步的来,从遍历每行开始。
#### 使用 `lines` 方法遍历每一行 #### 使用 `lines` 方法遍历每一行
Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如列表 12-17 这样工作: Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如示例 12-17 这样工作:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
@ -140,13 +140,13 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">列表 12-17遍历 `contents` 的每一行</span> <span class="caption">示例 12-17遍历 `contents` 的每一行</span>
`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 `lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-6 中见过使用迭代器的方法,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。
#### 用查询字符串搜索每一行 #### 用查询字符串搜索每一行
接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做 `contains` 的实用方法!如列表 12-18 所示在 `search` 函数中加入 `contains` 方法调用: 接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做 `contains` 的实用方法!如示例 12-18 所示在 `search` 函数中加入 `contains` 方法调用:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -160,11 +160,11 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">列表 12-18增加检查文本行是否包含 `query` 中字符串的功能</span> <span class="caption">示例 12-18增加检查文本行是否包含 `query` 中字符串的功能</span>
#### 存储匹配的行 #### 存储匹配的行
最后我们需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector列表 12-19 所示: 最后我们需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector示例 12-19 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -182,7 +182,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">列表 12-19储存匹配的行以便可以返回他们</span> <span class="caption">示例 12-19储存匹配的行以便可以返回他们</span>
现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试: 现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试:

View File

@ -10,7 +10,7 @@
首先,增加一个新函数,当设置了环境变量时会调用它。 首先,增加一个新函数,当设置了环境变量时会调用它。
这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如列表 12-20 所示: 这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如示例 12-20 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -51,15 +51,15 @@ Trust me.";
} }
``` ```
<span class="caption">列表 12-20为准备添加的大小写不敏感函数新增失败测试</span> <span class="caption">示例 12-20为准备添加的大小写不敏感函数新增失败测试</span>
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。 注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
大小写 **不敏感** 搜索的新测试使用 "rUsT" 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中“rUsT” 查询应该包含 “Rust:” 包含一个大写的 R 还有 “Trust me.” 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如列表 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。 大小写 **不敏感** 搜索的新测试使用 "rUsT" 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中“rUsT” 查询应该包含 “Rust:” 包含一个大写的 R 还有 “Trust me.” 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。
### 实现 `search_case_insensitive` 函数 ### 实现 `search_case_insensitive` 函数
`search_case_insensitive` 函数,如列表 12-21 所示,将与 `search` 函数基本相同。唯一的区别是它会将 `query` 变量和每一 `line` 都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。 `search_case_insensitive` 函数,如示例 12-21 所示,将与 `search` 函数基本相同。唯一的区别是它会将 `query` 变量和每一 `line` 都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -78,7 +78,7 @@ fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<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” 处理并对大小写不敏感。 首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 “rust”、“RUST”、“Rust” 或者 “rUsT”我们都将其当作 “rust” 处理并对大小写不敏感。
@ -108,7 +108,7 @@ pub struct Config {
} }
``` ```
这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如列表 12-22 所示: 这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如示例 12-22 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -151,9 +151,9 @@ pub fn run(config: Config) -> Result<(), Box<Error>>{
} }
``` ```
<span class="caption">列表 12-22根据 `config.case_sensitive` 的值调用 `search``search_case_insensitive`</span> <span class="caption">示例 12-22根据 `config.case_sensitive` 的值调用 `search``search_case_insensitive`</span>
最后需要实际检查环境变量。处理环境变量的函数位于标准库的 `env` 模块中,所以我们需要在 *src/lib.rs* 的开头增加一个 `use std::env;` 行将这个模块引入作用域中。接着在 `Config::new` 中使用 `env` 模块的 `var` 方法来检查一个叫做 `CASE_INSENSITIVE` 的环境变量,如列表 12-23 所示: 最后需要实际检查环境变量。处理环境变量的函数位于标准库的 `env` 模块中,所以我们需要在 *src/lib.rs* 的开头增加一个 `use std::env;` 行将这个模块引入作用域中。接着在 `Config::new` 中使用 `env` 模块的 `var` 方法来检查一个叫做 `CASE_INSENSITIVE` 的环境变量,如示例 12-23 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -183,13 +183,13 @@ impl Config {
} }
``` ```
<span class="caption">列表 12-23检查叫做 `CASE_INSENSITIVE` 的环境变量</span> <span class="caption">示例 12-23检查叫做 `CASE_INSENSITIVE` 的环境变量</span>
这里创建了一个新变量 `case_sensitive`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`CASE_INSENSITIVE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 成员,并在环境变量未被设置时返回 `Err` 成员。 这里创建了一个新变量 `case_sensitive`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`CASE_INSENSITIVE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 成员,并在环境变量未被设置时返回 `Err` 成员。
我们使用 `Result``is_err` 方法来检查其是否是一个 error也就是环境变量未被设置的情况这也就意味着我们 **需要** 进行一个大小写敏感搜索。如果`CASE_INSENSITIVE` 环境变量被设置为任何值,`is_err` 会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的 **值**,只关心它是否被设置了,所以检查 `is_err` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。 我们使用 `Result``is_err` 方法来检查其是否是一个 error也就是环境变量未被设置的情况这也就意味着我们 **需要** 进行一个大小写敏感搜索。如果`CASE_INSENSITIVE` 环境变量被设置为任何值,`is_err` 会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的 **值**,只关心它是否被设置了,所以检查 `is_err` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。
我们将变量 `case_sensitive` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者列表 12-22 中实现的 `search_case_insensitive` 我们将变量 `case_sensitive` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者示例 12-22 中实现的 `search_case_insensitive`
让我们试一试吧!首先不设置环境变量并使用查询 “to” 运行程序,这应该会匹配任何全小写的单词 “to” 的行: 让我们试一试吧!首先不设置环境变量并使用查询 “to” 运行程序,这应该会匹配任何全小写的单词 “to” 的行:

View File

@ -28,7 +28,7 @@ Problem parsing arguments: not enough arguments
### 将错误打印到标准错误 ### 将错误打印到标准错误
让我们如列表 12-24 所示的代码改变错误信息是如何被打印的。得益于本章早些时候的重构,所有打印错误信息的代码都位于 `main` 一个函数中。标准库提供了 `eprintln!` 宏来打印到标准错误流,所以将两个调用 `println!` 打印错误信息的维持替换为 `eprintln!` 让我们如示例 12-24 所示的代码改变错误信息是如何被打印的。得益于本章早些时候的重构,所有打印错误信息的代码都位于 `main` 一个函数中。标准库提供了 `eprintln!` 宏来打印到标准错误流,所以将两个调用 `println!` 打印错误信息的维持替换为 `eprintln!`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -49,7 +49,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 12-24使用 `eprintln!` 将错误信息写入标准错误而不是标准输出</span> <span class="caption">示例 12-24使用 `eprintln!` 将错误信息写入标准错误而不是标准输出</span>
`println!` 改为 `eprintln!` 之后,让我们再次尝试用同样的方式运行程序,不使用任何参数并通过 `>` 重定向标准输出: `println!` 改为 `eprintln!` 之后,让我们再次尝试用同样的方式运行程序,不使用任何参数并通过 `>` 重定向标准输出:

View File

@ -10,7 +10,7 @@ Rust 的闭包是可以保存进变量或作为参数传递给其他函数的匿
让我们看看一个展示储存闭包并在之后执行如何有用的情形的例子。其间我们会讨论闭包的语法、类型推断和 trait。 让我们看看一个展示储存闭包并在之后执行如何有用的情形的例子。其间我们会讨论闭包的语法、类型推断和 trait。
这个假想的情况如下:我们在一个通过 app 生成自定义健身计划的初创企业工作。其后端使用 Rust 编写而生成健身计划的算法需要考虑很多不同的因素比如用户的年龄、身体质量指数Body Mass Index、用户喜好、最近的健身活动和用户指定的强度系数。本例中实际的算法并不重要。我们只希望在需要时调用算法并且只希望调用一次这样就不会让用户等得太久。这里将通过调用 `simulated_expensive_calculation` 函数来模拟调用假象的算法,如列表 13-1 所示,它会打印出 `calculating slowly...`,等待两秒,并接着返回传递给它的数字: 这个假想的情况如下:我们在一个通过 app 生成自定义健身计划的初创企业工作。其后端使用 Rust 编写而生成健身计划的算法需要考虑很多不同的因素比如用户的年龄、身体质量指数Body Mass Index、用户喜好、最近的健身活动和用户指定的强度系数。本例中实际的算法并不重要。我们只希望在需要时调用算法并且只希望调用一次这样就不会让用户等得太久。这里将通过调用 `simulated_expensive_calculation` 函数来模拟调用假象的算法,如示例 13-1 所示,它会打印出 `calculating slowly...`,等待两秒,并接着返回传递给它的数字:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -25,7 +25,7 @@ fn simulated_expensive_calculation(intensity: i32) -> i32 {
} }
``` ```
<span class="caption">列表 13-1一个用来代替假象计算的函数它大约会执行两秒</span> <span class="caption">示例 13-1一个用来代替假象计算的函数它大约会执行两秒</span>
接下来,`main` 函数中将会包含本例的健身 app 中的重要部分。这代表当用户请求健身计划时 app 会调用的代码。因为与 app 前端的交互与闭包的使用并不相关,所以我们将硬编码代表程序输入的值并打印输出。 接下来,`main` 函数中将会包含本例的健身 app 中的重要部分。这代表当用户请求健身计划时 app 会调用的代码。因为与 app 前端的交互与闭包的使用并不相关,所以我们将硬编码代表程序输入的值并打印输出。
@ -36,7 +36,7 @@ fn simulated_expensive_calculation(intensity: i32) -> i32 {
程序的输出将会是建议的锻炼计划。 程序的输出将会是建议的锻炼计划。
列表 13-2 展示了我们将要使用的 `main` 函数。处于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: 示例 13-2 展示了我们将要使用的 `main` 函数。处于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -50,9 +50,9 @@ fn main() {
# fn generate_workout(intensity: i32, random_number: i32) {} # fn generate_workout(intensity: i32, random_number: i32) {}
``` ```
<span class="caption">列表 13-2`main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入</span> <span class="caption">示例 13-2`main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入</span>
这就是我们的执行上下文。列表 13-3 中的 `generate_workout` 函数包含我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中: 这就是我们的执行上下文。示例 13-3 中的 `generate_workout` 函数包含我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -89,9 +89,9 @@ fn generate_workout(intensity: i32, random_number: i32) {
} }
``` ```
<span class="caption">列表 13-3程序的业务逻辑它根据输入并调用 `simulated_expensive_calculation` 函数来打印出健身计划</span> <span class="caption">示例 13-3程序的业务逻辑它根据输入并调用 `simulated_expensive_calculation` 函数来打印出健身计划</span>
列表 13-3 中的代码有多处慢计算函数的调用。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全没有调用它,`else` 中的 `else` 调用了它一次。 示例 13-3 中的代码有多处慢计算函数的调用。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全没有调用它,`else` 中的 `else` 调用了它一次。
`generate_workout` 函数的合意的行为是首先检查用户需要低强度(由小于 25 的系数代表锻炼还是高强度25 或以上)锻炼。低强度锻炼计划会根据由 `simulated_expensive_calculation` 函数所模拟的复杂算法建议一定数量的俯卧撑和仰卧起坐,此函数需要强度系数作为输入。 `generate_workout` 函数的合意的行为是首先检查用户需要低强度(由小于 25 的系数代表锻炼还是高强度25 或以上)锻炼。低强度锻炼计划会根据由 `simulated_expensive_calculation` 函数所模拟的复杂算法建议一定数量的俯卧撑和仰卧起坐,此函数需要强度系数作为输入。
@ -99,7 +99,7 @@ fn generate_workout(intensity: i32, random_number: i32) {
数据科学部门的同学告知我们必须对调用算法的方式做出一些改变。为了简化做出这些改变的更新,我们将重构代码来只调用 `simulated_expensive_calculation` 一次。同时还希望去掉目前多余的连续两次函数调用,并不希望在计算过程中增加任何其他此函数的调用。也就是说,我们不希望在完全无需其结果的情况调用函数,不过最终仍然需要调用函数一次。 数据科学部门的同学告知我们必须对调用算法的方式做出一些改变。为了简化做出这些改变的更新,我们将重构代码来只调用 `simulated_expensive_calculation` 一次。同时还希望去掉目前多余的连续两次函数调用,并不希望在计算过程中增加任何其他此函数的调用。也就是说,我们不希望在完全无需其结果的情况调用函数,不过最终仍然需要调用函数一次。
有多种方法可以重构此程序。我们首先尝试的是将重复的慢计算函数调用提取到一个变量中,如列表 13-4 所示: 有多种方法可以重构此程序。我们首先尝试的是将重复的慢计算函数调用提取到一个变量中,如示例 13-4 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -139,7 +139,7 @@ fn generate_workout(intensity: i32, random_number: i32) {
} }
``` ```
<span class="caption">列表 13-4`simulated_expensive_calculation` 调用提取到一个位置,位于 `if` 块之前并将结果储存在变量 `expensive_result`</span> <span class="caption">示例 13-4`simulated_expensive_calculation` 调用提取到一个位置,位于 `if` 块之前并将结果储存在变量 `expensive_result`</span>
这个修改统一了 `simulated_expensive_calculation` 调用并解决了第一个 `if` 块中不必要的两次调用函数的问题。不幸的是,现在所有的情况下都需要调用函数并等待结果,而内部 `if` 块完全不需要其结果。 这个修改统一了 `simulated_expensive_calculation` 调用并解决了第一个 `if` 块中不必要的两次调用函数的问题。不幸的是,现在所有的情况下都需要调用函数并等待结果,而内部 `if` 块完全不需要其结果。
@ -147,7 +147,7 @@ fn generate_workout(intensity: i32, random_number: i32) {
### 闭包储存了之后会执行的代码 ### 闭包储存了之后会执行的代码
不同于总是在 `if` 块之前调用 `simulated_expensive_calculation` 函数并储存其结果,我们可以定义一个闭包并将其储存在变量中,如列表 13-5 所示。实际上可以选择将整个 `simulated_expensive_calculation` 函数体移动到这里引入的闭包中: 不同于总是在 `if` 块之前调用 `simulated_expensive_calculation` 函数并储存其结果,我们可以定义一个闭包并将其储存在变量中,如示例 13-5 所示。实际上可以选择将整个 `simulated_expensive_calculation` 函数体移动到这里引入的闭包中:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -163,7 +163,7 @@ let expensive_closure = |num| {
# expensive_closure(5); # expensive_closure(5);
``` ```
<span class="caption">列表 13-5使用慢计算的函数体定义一个闭包并储存到变量 `expensive_closure`</span> <span class="caption">示例 13-5使用慢计算的函数体定义一个闭包并储存到变量 `expensive_closure`</span>
闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始。在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|` 闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始。在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`
@ -171,7 +171,7 @@ let expensive_closure = |num| {
注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。 注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。
现在我们定义了闭包,可以改变 `if` 块中的代码来调用闭包以执行代码并获取结果值。调用闭包看起来非常类似调用函数;指定存放闭包定义的变量名并后跟包含期望使用的参数的括号,如列表 13-6 所示: 现在我们定义了闭包,可以改变 `if` 块中的代码来调用闭包以执行代码并获取结果值。调用闭包看起来非常类似调用函数;指定存放闭包定义的变量名并后跟包含期望使用的参数的括号,如示例 13-6 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -208,9 +208,9 @@ fn generate_workout(intensity: i32, random_number: i32) {
} }
``` ```
<span class="caption">列表 13-6调用定义的 `expensive_closure`</span> <span class="caption">示例 13-6调用定义的 `expensive_closure`</span>
现在我们达成了将满计算统一到一个地方的目标,并只会在需要结果的时候执行改代码。然而,我们又重新引入了列表 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上;首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 现在我们达成了将满计算统一到一个地方的目标,并只会在需要结果的时候执行改代码。然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上;首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。
### 闭包类型推断和注解 ### 闭包类型推断和注解
@ -220,7 +220,7 @@ fn generate_workout(intensity: i32, random_number: i32) {
另外,闭包通常很短并只与对应相对任意的场景较小的上下文中。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。 另外,闭包通常很短并只与对应相对任意的场景较小的上下文中。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。
类似于变量,如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型注解;为列表 13-4 中定义的闭包标注类型将看起来像列表 13-7 中的定义: 类似于变量,如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型注解;为示例 13-4 中定义的闭包标注类型将看起来像示例 13-7 中的定义:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -235,7 +235,7 @@ let expensive_closure = |num: i32| -> i32 {
}; };
``` ```
<span class="caption">列表 13-7为闭包的参数和返回值增加可选的类型注解</span> <span class="caption">示例 13-7为闭包的参数和返回值增加可选的类型注解</span>
有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对其相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法: 有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对其相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法:
@ -248,7 +248,7 @@ let add_one_v4 = |x| x + 1 ;
第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。 第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。
闭包定义会为每个参数和返回值推断一个具体类型。例如,列表 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第一次使用 `i32`,则会得到一个错误: 闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第一次使用 `i32`,则会得到一个错误:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -259,7 +259,7 @@ let s = example_closure(String::from("hello"));
let n = example_closure(5); let n = example_closure(5);
``` ```
<span class="caption">列表 13-8尝试调用一个被推断为两个不同类型的闭包</span> <span class="caption">示例 13-8尝试调用一个被推断为两个不同类型的闭包</span>
编译器给出如下错误: 编译器给出如下错误:
@ -279,7 +279,7 @@ error[E0308]: mismatched types
### 使用带有泛型和 `Fn` trait 的闭包 ### 使用带有泛型和 `Fn` trait 的闭包
回到我们的健身计划生成 app ,在列表 13-6 中的代码仍然调用了多于需要的慢计算闭包。在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。 回到我们的健身计划生成 app ,在示例 13-6 中的代码仍然调用了多于需要的慢计算闭包。在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。
然而,因为拥有一个慢计算的闭包,我们还可以采取另一个解决方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization**lazy evaluation* 然而,因为拥有一个慢计算的闭包,我们还可以采取另一个解决方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization**lazy evaluation*
@ -289,7 +289,7 @@ error[E0308]: mismatched types
为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `i32` 的参数并返回一个 `i32`,这样所指定的 trait bound 就是 `Fn(i32) -> i32` 为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `i32` 的参数并返回一个 `i32`,这样所指定的 trait bound 就是 `Fn(i32) -> i32`
列表 13-9 展示了存放了闭包和一个 Option 结果值的 `Cacher` 结构体的定义: 示例 13-9 展示了存放了闭包和一个 Option 结果值的 `Cacher` 结构体的定义:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -302,13 +302,13 @@ struct Cacher<T>
} }
``` ```
<span class="caption">列表 13-9定义一个 `Cacher` 结构体来在 `calculation` 中存放闭包并在 `value` 中存放 Option 值</span> <span class="caption">示例 13-9定义一个 `Cacher` 结构体来在 `calculation` 中存放闭包并在 `value` 中存放 Option 值</span>
结果提 `Cacher` 有一个泛型 `T` 的字段 `calculation`。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `i32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `i32`(由 `->` 之后的内容)。 结果提 `Cacher` 有一个泛型 `T` 的字段 `calculation`。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `i32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `i32`(由 `->` 之后的内容)。
`value``Option<i32>` 类型的。在执行闭包之前,`value` 将是 `None`。如果使用 `Cacher` 的代码请求闭包的结果,这时会执行闭包并将结果储存在 `value` 字段的 `Some` 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 `Some` 成员中的结果。 `value``Option<i32>` 类型的。在执行闭包之前,`value` 将是 `None`。如果使用 `Cacher` 的代码请求闭包的结果,这时会执行闭包并将结果储存在 `value` 字段的 `Some` 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 `Some` 成员中的结果。
刚才讨论的油管 `value` 字段逻辑定义于列表 13-10 刚才讨论的油管 `value` 字段逻辑定义于示例 13-10
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -343,7 +343,7 @@ impl<T> Cacher<T>
} }
``` ```
<span class="caption">列表 13-10一个 `Cacher` 的关联函数 `new` 和管理缓存逻辑的 `value` 方法的实现</span> <span class="caption">示例 13-10一个 `Cacher` 的关联函数 `new` 和管理缓存逻辑的 `value` 方法的实现</span>
`Cacher` 结构体的字段是私有的,因为我们希望 `Cacher` 管理这些值而不是任由调用代码潜在的直接改变他们。`Cacher::new` 函数获取一个泛型参数 `T`,它定义于 `impl` 块上下文中并与 `Cacher` 结构体有着相同的 trait bound。`Cacher::new` 返回一个在 `calculation` 字段中存放了指定闭包和在 `value` 字段中存放了 `None` 值的 `Cacher` 实例,因为我们还未执行闭包。 `Cacher` 结构体的字段是私有的,因为我们希望 `Cacher` 管理这些值而不是任由调用代码潜在的直接改变他们。`Cacher::new` 函数获取一个泛型参数 `T`,它定义于 `impl` 块上下文中并与 `Cacher` 结构体有着相同的 trait bound。`Cacher::new` 返回一个在 `calculation` 字段中存放了指定闭包和在 `value` 字段中存放了 `None` 值的 `Cacher` 实例,因为我们还未执行闭包。
@ -351,7 +351,7 @@ impl<T> Cacher<T>
如果 `self.value``None`,则会调用 `self.calculation` 中储存的闭包,将结果保存到 `self.value` 以便将来使用,并同时返回结果值。 如果 `self.value``None`,则会调用 `self.calculation` 中储存的闭包,将结果保存到 `self.value` 以便将来使用,并同时返回结果值。
列表 13-11 展示了如何在列表 13-6 的 `generate_workout` 函数中利用 `Cacher` 结构体: 示例 13-11 展示了如何在示例 13-6 的 `generate_workout` 函数中利用 `Cacher` 结构体:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -417,9 +417,9 @@ fn generate_workout(intensity: i32, random_number: i32) {
} }
``` ```
<span class="caption">列表 13-11`generate_workout` 函数中利用 `Cacher` 结构体来抽象出缓存逻辑</span> <span class="caption">示例 13-11`generate_workout` 函数中利用 `Cacher` 结构体来抽象出缓存逻辑</span>
不同于直接将闭包保存进一个变量,我们保存一个新的 `Cacher` 实例来存放闭包。接着,在每一个需要结果的地方,调用 `Cacher` 实例的 `value` 方法。可以调用 `value` 方法任意多次,或者一次也不调用,而慢计算最多只会运行一次。尝试使用列表 13-2 中的 `main` 函数来运行这段程序,并将 `simulated_user_specified_value``simulated_random_number` 变量中的值来验证在所有情况下在多个 `if``else` 块中,闭包打印的 `calculating slowly...` 只会在需要时出现并只会出现一次。 不同于直接将闭包保存进一个变量,我们保存一个新的 `Cacher` 实例来存放闭包。接着,在每一个需要结果的地方,调用 `Cacher` 实例的 `value` 方法。可以调用 `value` 方法任意多次,或者一次也不调用,而慢计算最多只会运行一次。尝试使用示例 13-2 中的 `main` 函数来运行这段程序,并将 `simulated_user_specified_value``simulated_random_number` 变量中的值来验证在所有情况下在多个 `if``else` 块中,闭包打印的 `calculating slowly...` 只会在需要时出现并只会出现一次。
`Cacher` 负责确保不会调用超过所需的慢计算所需的逻辑,这样 `generate_workout` 就可以专注业务逻辑了。值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些小问题,这使得在不同上下文中复用变得很困难。 `Cacher` 负责确保不会调用超过所需的慢计算所需的逻辑,这样 `generate_workout` 就可以专注业务逻辑了。值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些小问题,这使得在不同上下文中复用变得很困难。
@ -439,7 +439,7 @@ fn call_with_different_values() {
这个测试使用返回传递给它的值的闭包创建了一个新的 `Cacher` 实例。使用为 1 的 `arg` 和为 2 的 `arg` 调用 `Cacher` 实例的 `value` 方法,同时我们期望使用为 2 的 `arg` 调用 `value` 会返回 2。 这个测试使用返回传递给它的值的闭包创建了一个新的 `Cacher` 实例。使用为 1 的 `arg` 和为 2 的 `arg` 调用 `Cacher` 实例的 `value` 方法,同时我们期望使用为 2 的 `arg` 调用 `value` 会返回 2。
使用列表 13-9 和列表 13-10 的 `Cacher` 实现运行测试,它会在 `assert_eq!` 失败并显示如下信息: 使用示例 13-9 和示例 13-10 的 `Cacher` 实现运行测试,它会在 `assert_eq!` 失败并显示如下信息:
```text ```text
thread 'call_with_different_arg_values' panicked at 'assertion failed: thread 'call_with_different_arg_values' panicked at 'assertion failed:
@ -456,7 +456,7 @@ thread 'call_with_different_arg_values' panicked at 'assertion failed:
在健身计划生成器的例子中,我们只将闭包作为内联匿名函数来使用。不过闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问定义他们的作用域的变量。 在健身计划生成器的例子中,我们只将闭包作为内联匿名函数来使用。不过闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问定义他们的作用域的变量。
列表 13-12 有一个储存在 `equal_to_x` 变量中闭包的例子,它使用了闭包环境中的变量 `x` 示例 13-12 有一个储存在 `equal_to_x` 变量中闭包的例子,它使用了闭包环境中的变量 `x`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -472,7 +472,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 13-12一个引用了其周围作用域中变量的闭包示例</span> <span class="caption">示例 13-12一个引用了其周围作用域中变量的闭包示例</span>
这里,即便 `x` 并不是 `equal_to_x` 的一个参数,`equal_to_x` 闭包也被允许使用变量 `x`,因为它与 `equal_to_x` 定义于相同的作用域。 这里,即便 `x` 并不是 `equal_to_x` 的一个参数,`equal_to_x` 闭包也被允许使用变量 `x`,因为它与 `equal_to_x` 定义于相同的作用域。
@ -513,9 +513,9 @@ closure form instead
* `Fn` 从其环境不可变的借用值 * `Fn` 从其环境不可变的借用值
* `FnMut` 可变的借用值所以可以改变其环境 * `FnMut` 可变的借用值所以可以改变其环境
当创建一个闭包时Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。在列表 13-12 中,`equal_to_x` 闭包不可变的借用了 `x`(所以 `equal_to_x` 使用 `Fn` trait因为闭包体只需要读取 `x` 的值。 当创建一个闭包时Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。在示例 13-12 中,`equal_to_x` 闭包不可变的借用了 `x`(所以 `equal_to_x` 使用 `Fn` trait因为闭包体只需要读取 `x` 的值。
如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这在将闭包传递给新线程以便将数据移动到新线程中时最为实用。第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了列表 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动: 如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这在将闭包传递给新线程以便将数据移动到新线程中时最为实用。第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>

View File

@ -6,7 +6,7 @@
迭代器模式允许你对一个项的序列进行某些处理。**迭代器***iterator*)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。 迭代器模式允许你对一个项的序列进行某些处理。**迭代器***iterator*)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。
在 Rust 中,迭代器是 **惰性的***lazy*),这意味着直到调用方法消费迭代器之前它都不会有效果。例如,列表 13-13 中的代码通过调用定义于 `Vec` 上的 `iter` 方法在一个 vector `v1` 上创建了一个迭代器。这段代码本身没有任何用处: 在 Rust 中,迭代器是 **惰性的***lazy*),这意味着直到调用方法消费迭代器之前它都不会有效果。例如,示例 13-13 中的代码通过调用定义于 `Vec` 上的 `iter` 方法在一个 vector `v1` 上创建了一个迭代器。这段代码本身没有任何用处:
```rust ```rust
let v1 = vec![1, 2, 3]; let v1 = vec![1, 2, 3];
@ -14,9 +14,9 @@ let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); let v1_iter = v1.iter();
``` ```
<span class="caption">列表 13-13创建了迭代器</span> <span class="caption">示例 13-13创建了迭代器</span>
创建迭代器之后,可以选择用多种方式利用它。在列表 3-6 中,我们实际上使用了迭代器和 `for` 循环在每一个项上执行了一些代码,直到现在我们才解释了 `iter` 调用做了什么。列表 13-14 中的例子将迭代器的创建和 `for` 循环中的使用分开。迭代器被储存在 `v1_iter` 变量中,而这时没有进行迭代。一旦 `for` 循环开始使用 `v1_iter`,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值: 创建迭代器之后,可以选择用多种方式利用它。在示例 3-6 中,我们实际上使用了迭代器和 `for` 循环在每一个项上执行了一些代码,直到现在我们才解释了 `iter` 调用做了什么。示例 13-14 中的例子将迭代器的创建和 `for` 循环中的使用分开。迭代器被储存在 `v1_iter` 变量中,而这时没有进行迭代。一旦 `for` 循环开始使用 `v1_iter`,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值:
```rust ```rust
let v1 = vec![1, 2, 3]; let v1 = vec![1, 2, 3];
@ -28,7 +28,7 @@ for val in v1_iter {
} }
``` ```
<span class="caption">列表 13-14在一个 `for` 循环中使用迭代器</span> <span class="caption">示例 13-14在一个 `for` 循环中使用迭代器</span>
在标准库中没有提供迭代器的语言中,我们可能会使用一个从 0 开始的索引变量,使用这个变量索引 vector 中的值,并循环增加其值直到达到 vector 的元素数量。迭代器为我们处理了所有这些逻辑,这减少了重复代码并潜在的消除了混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构.让我们看看迭代器是如何做到这些的。 在标准库中没有提供迭代器的语言中,我们可能会使用一个从 0 开始的索引变量,使用这个变量索引 vector 中的值,并循环增加其值直到达到 vector 的元素数量。迭代器为我们处理了所有这些逻辑,这减少了重复代码并潜在的消除了混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构.让我们看看迭代器是如何做到这些的。
@ -48,7 +48,7 @@ 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` 方法;列表 13-15 有一个测试展示了重复调用由 vector 创建的迭代器的 `next` 方法所得到的值: `next``Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。如果你希望的话可以直接调用迭代器的 `next` 方法;示例 13-15 有一个测试展示了重复调用由 vector 创建的迭代器的 `next` 方法所得到的值:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -66,7 +66,7 @@ fn iterator_demonstration() {
} }
``` ```
<span class="caption">列表 13-15在迭代器上直接调用 `next` 方法</span> <span class="caption">示例 13-15在迭代器上直接调用 `next` 方法</span>
注意 `v1_iter` 需要是可变的:在迭代器上调用 `next` 方法改变了迭代器中用来记录序列位置的状态。换句话说,代码 **消费**consume或使用了迭代器。每一个 `next` 调用都会从迭代器中吃掉一个项。使用 `for` 循环时无需使 `v1_iter` 可变因为 `for` 循环会获取 `v1_iter` 的所有权并在后台使 `v1_iter` 可变。 注意 `v1_iter` 需要是可变的:在迭代器上调用 `next` 方法改变了迭代器中用来记录序列位置的状态。换句话说,代码 **消费**consume或使用了迭代器。每一个 `next` 调用都会从迭代器中吃掉一个项。使用 `for` 循环时无需使 `v1_iter` 可变因为 `for` 循环会获取 `v1_iter` 的所有权并在后台使 `v1_iter` 可变。
@ -76,7 +76,7 @@ fn iterator_demonstration() {
`Iterator` trait 有一系列不同的由标准库提供默认实现的方法;你可以在 `Iterator` trait 的标准库 API 文档中找到所有这些方法。一些方法在其定义中调用了 `next` 方法,这也就是为什么在实现 `Iterator` trait 时要求实现 `next` 方法的原因。 `Iterator` trait 有一系列不同的由标准库提供默认实现的方法;你可以在 `Iterator` trait 的标准库 API 文档中找到所有这些方法。一些方法在其定义中调用了 `next` 方法,这也就是为什么在实现 `Iterator` trait 时要求实现 `next` 方法的原因。
这些调用 `next` 方法的方法被称为 **消费适配器***consuming adaptors*),因为调用他们会消耗迭代器。一个消费适配器的例子是 `sum` 方法。这个方法获取迭代器的所有权并反复调用 `next` 来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。列表 13-16 有一个展示 `sum` 方法使用的测试: 这些调用 `next` 方法的方法被称为 **消费适配器***consuming adaptors*),因为调用他们会消耗迭代器。一个消费适配器的例子是 `sum` 方法。这个方法获取迭代器的所有权并反复调用 `next` 来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。示例 13-16 有一个展示 `sum` 方法使用的测试:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -93,13 +93,13 @@ fn iterator_sum() {
} }
``` ```
<span class="caption">列表 13-16调用 `sum` 方法获取迭代器所有项的总和</span> <span class="caption">示例 13-16调用 `sum` 方法获取迭代器所有项的总和</span>
调用 `sum` 之后不再允许使用 `v1_iter` 因为调用 `sum` 时它会获取迭代器的所有权。 调用 `sum` 之后不再允许使用 `v1_iter` 因为调用 `sum` 时它会获取迭代器的所有权。
### `Iterator` trait 中产生其他迭代器的方法 ### `Iterator` trait 中产生其他迭代器的方法
`Iterator` trait 中定义的另一类方法会产生其他的迭代器。这些方法被称为 **迭代器适配器***iterator adaptors*),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。列表 13-17 展示了一个调用迭代器适配器方法 `map` 的例子,它会获取一个 `map` 会在每一个项上调用的闭包来产生一个新迭代器,它的每一项为 vector 中每一项加一。不过这些代码会产生一个警告: `Iterator` trait 中定义的另一类方法会产生其他的迭代器。这些方法被称为 **迭代器适配器***iterator adaptors*),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。示例 13-17 展示了一个调用迭代器适配器方法 `map` 的例子,它会获取一个 `map` 会在每一个项上调用的闭包来产生一个新迭代器,它的每一项为 vector 中每一项加一。不过这些代码会产生一个警告:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -109,7 +109,7 @@ let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1); v1.iter().map(|x| x + 1);
``` ```
<span class="caption">列表 13-17调用迭代器适配器 `map` 来创建一个新迭代器</span> <span class="caption">示例 13-17调用迭代器适配器 `map` 来创建一个新迭代器</span>
得到的警告是: 得到的警告是:
@ -124,9 +124,9 @@ nothing unless consumed
= note: #[warn(unused_must_use)] on by default = note: #[warn(unused_must_use)] on by default
``` ```
列表 13-17 中的代码实际上并没有做任何事;所指定的闭包从未被调用过。警告提醒了我们为什么:迭代器适配器是惰性的,而这里我们可能意在消费迭代器。 示例 13-17 中的代码实际上并没有做任何事;所指定的闭包从未被调用过。警告提醒了我们为什么:迭代器适配器是惰性的,而这里我们可能意在消费迭代器。
为了修复这个警告并消费迭代器获取有用的结果,我们将使用第十二章简要讲到的 `collect` 方法。这个方法消费迭代器并将结果收集到一个数据结构中。在列表 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加一的结果: 为了修复这个警告并消费迭代器获取有用的结果,我们将使用第十二章简要讲到的 `collect` 方法。这个方法消费迭代器并将结果收集到一个数据结构中。在示例 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加一的结果:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -138,13 +138,13 @@ let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]); assert_eq!(v2, vec![2, 3, 4]);
``` ```
<span class="caption">列表 13-18调用 `map` 方法创建一个新迭代器,接着调用 `collect` 方法消费新迭代器并创建一个 vector</span> <span class="caption">示例 13-18调用 `map` 方法创建一个新迭代器,接着调用 `collect` 方法消费新迭代器并创建一个 vector</span>
因为 `map` 获取一个闭包,可以指定任何希望在遍历的每个元素上执行的操作。这是一个展示如何使用闭包来自定义行为同时又复用 `Iterator` trait 提供的迭代行为的绝佳例子。 因为 `map` 获取一个闭包,可以指定任何希望在遍历的每个元素上执行的操作。这是一个展示如何使用闭包来自定义行为同时又复用 `Iterator` trait 提供的迭代行为的绝佳例子。
### 使用闭包获取环境与迭代器 ### 使用闭包获取环境与迭代器
现在我们介绍了迭代器,让我们展示一个通过使用 `filter` 迭代器适配器和捕获环境的闭包的常规用例。迭代器的 `filter` 方法获取一个使用迭代器的每一个项并返回布尔值的闭包。如果闭包返回 `true`,其值将会包含在 `filter` 提供的新迭代器中。如果闭包返回 `false`,其值不会包含在结果迭代器中。列表 13-19 展示了使用 `filter` 和一个捕获环境中变量 `shoe_size` 的闭包,这样闭包就可以遍历一个 `Shoe` 结构体集合以便只返回指定大小的鞋子: 现在我们介绍了迭代器,让我们展示一个通过使用 `filter` 迭代器适配器和捕获环境的闭包的常规用例。迭代器的 `filter` 方法获取一个使用迭代器的每一个项并返回布尔值的闭包。如果闭包返回 `true`,其值将会包含在 `filter` 提供的新迭代器中。如果闭包返回 `false`,其值不会包含在结果迭代器中。示例 13-19 展示了使用 `filter` 和一个捕获环境中变量 `shoe_size` 的闭包,这样闭包就可以遍历一个 `Shoe` 结构体集合以便只返回指定大小的鞋子:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -181,7 +181,7 @@ fn filters_by_size() {
} }
``` ```
<span class="caption">列表 13-19使用 `filter` 方法和一个捕获 `shoe_size` 的闭包</span> <span class="caption">示例 13-19使用 `filter` 方法和一个捕获 `shoe_size` 的闭包</span>
`shoes_in_my_size` 函数获取一个鞋子 vector 的所有权和一个鞋子大小作为参数。它返回一个只包含指定大小鞋子的 vector。在 `shoes_in_my_size` 函数体中调用了 `into_iter` 来创建一个获取 vector 所有权的迭代器。接着调用 `filter` 将这个迭代器适配成只含有闭包返回 `true` 元素的新迭代器。我们指定的闭包从环境中捕获了 `shoe_size` 变量并使用其值与每一只鞋的大小作比较,只保留指定大小的鞋子。最终,调用 `collect` 将迭代器适配器返回的值收集进一个 vector 并返回。 `shoes_in_my_size` 函数获取一个鞋子 vector 的所有权和一个鞋子大小作为参数。它返回一个只包含指定大小鞋子的 vector。在 `shoes_in_my_size` 函数体中调用了 `into_iter` 来创建一个获取 vector 所有权的迭代器。接着调用 `filter` 将这个迭代器适配成只含有闭包返回 `true` 元素的新迭代器。我们指定的闭包从环境中捕获了 `shoe_size` 变量并使用其值与每一只鞋的大小作比较,只保留指定大小的鞋子。最终,调用 `collect` 将迭代器适配器返回的值收集进一个 vector 并返回。
@ -193,7 +193,7 @@ fn filters_by_size() {
我们将要创建的迭代器只会从 1 数到 5。首先我们会创建一个结构体来存放一些值接着实现 `Iterator` trait 将这个结构体放入迭代器中并在此实现中使用其值。 我们将要创建的迭代器只会从 1 数到 5。首先我们会创建一个结构体来存放一些值接着实现 `Iterator` trait 将这个结构体放入迭代器中并在此实现中使用其值。
列表 13-20 有一个 `Counter` 结构体定义和一个创建 `Counter` 实例的关联函数 `new` 示例 13-20 有一个 `Counter` 结构体定义和一个创建 `Counter` 实例的关联函数 `new`
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -210,11 +210,11 @@ impl Counter {
} }
``` ```
<span class="caption">列表 13-20定义 `Counter` 结构体和一个创建 `count` 初值为 0 的 `Counter` 实例的 `new` 函数</span> <span class="caption">示例 13-20定义 `Counter` 结构体和一个创建 `count` 初值为 0 的 `Counter` 实例的 `new` 函数</span>
`Counter` 结构体有一个字段 `count`。这个字段存放一个 `u32` 值,它会记录处理 1 到 5 的迭代过程中的位置。`count` 是私有的因为我们希望 `Counter` 的实现来管理这个值。`new` 函数通过总是从为 0 的 `count` 字段开始新实例来确保我们需要的行为。 `Counter` 结构体有一个字段 `count`。这个字段存放一个 `u32` 值,它会记录处理 1 到 5 的迭代过程中的位置。`count` 是私有的因为我们希望 `Counter` 的实现来管理这个值。`new` 函数通过总是从为 0 的 `count` 字段开始新实例来确保我们需要的行为。
接下来将为 `Counter` 类型实现 `Iterator` trait通过定义 `next` 方法来指定使用迭代器时的行为,如列表 13-21 所示: 接下来将为 `Counter` 类型实现 `Iterator` trait通过定义 `next` 方法来指定使用迭代器时的行为,如示例 13-21 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -238,13 +238,13 @@ impl Iterator for Counter {
} }
``` ```
<span class="caption">列表 13-21`Counter` 结构体上实现 `Iterator` trait</span> <span class="caption">示例 13-21`Counter` 结构体上实现 `Iterator` trait</span>
这里将迭代器的关联类型 `Item` 设置为 `u32`,意味着迭代器会返回 `u32` 值集合。再一次,这里仍无需担心关联类型,第十九章会讲到。我们希望迭代器对其内部状态加一,这也就是为何将 `count` 初始化为 0我们希望迭代器首先返回 1。如果 `count` 值小于 6`next` 会返回封装在 `Some` 中的当前值,不过如果 `count` 大于或等于 6迭代器会返回 `None` 这里将迭代器的关联类型 `Item` 设置为 `u32`,意味着迭代器会返回 `u32` 值集合。再一次,这里仍无需担心关联类型,第十九章会讲到。我们希望迭代器对其内部状态加一,这也就是为何将 `count` 初始化为 0我们希望迭代器首先返回 1。如果 `count` 值小于 6`next` 会返回封装在 `Some` 中的当前值,不过如果 `count` 大于或等于 6迭代器会返回 `None`
#### 使用 `Counter` 迭代器的 `next` 方法 #### 使用 `Counter` 迭代器的 `next` 方法
一旦实现了 `Iterator` trait我们就有了一个迭代器列表 13-22 展示了一个测试用来演示现在我们可以使用 `Counter` 结构体的迭代器功能,通过直接调用 `next` 方法,正如列表 13-15 中从 vector 创建的迭代器那样: 一旦实现了 `Iterator` trait我们就有了一个迭代器示例 13-22 展示了一个测试用来演示现在我们可以使用 `Counter` 结构体的迭代器功能,通过直接调用 `next` 方法,正如示例 13-15 中从 vector 创建的迭代器那样:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -280,7 +280,7 @@ fn calling_next_directly() {
} }
``` ```
<span class="caption">列表 13-22测试 `next` 方法实现的功能</span> <span class="caption">示例 13-22测试 `next` 方法实现的功能</span>
这个测试在 `counter` 变量中新建了一个 `Counter` 实例并接着反复调用 `next` 方法,来验证我们实现的行为符合这个迭代器返回从 1 到 5 的值的预期。 这个测试在 `counter` 变量中新建了一个 `Counter` 实例并接着反复调用 `next` 方法,来验证我们实现的行为符合这个迭代器返回从 1 到 5 的值的预期。
@ -288,7 +288,7 @@ fn calling_next_directly() {
通过定义 `next` 方法实现 `Iterator` trait我们现在就可以使用任何标准库定义的拥有默认实现的 `Iterator` trait 方法了,因为他们都使用了 `next` 方法的功能。 通过定义 `next` 方法实现 `Iterator` trait我们现在就可以使用任何标准库定义的拥有默认实现的 `Iterator` trait 方法了,因为他们都使用了 `next` 方法的功能。
例如,出于某种原因我们希望获取 `Counter` 实例产生的值,将这些值与另一个 `Counter` 实例在省略了第一个值之后产生的值配对,将每一对值相乘,只保留那些可以被三整除的结果,然后将所有保留的结果相加,这可以如列表 13-23 中的测试这样做: 例如,出于某种原因我们希望获取 `Counter` 实例产生的值,将这些值与另一个 `Counter` 实例在省略了第一个值之后产生的值配对,将每一对值相乘,只保留那些可以被三整除的结果,然后将所有保留的结果相加,这可以如示例 13-23 中的测试这样做:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -330,7 +330,7 @@ fn using_other_iterator_trait_methods() {
} }
``` ```
<span class="caption">列表 13-23使用自定义的 `Counter` 迭代器的多种方法</span> <span class="caption">示例 13-23使用自定义的 `Counter` 迭代器的多种方法</span>
注意 `zip` 只产生4对值理论上第五对值 `(5, None)` 从未被产生,因为 `zip` 在任一输入迭代器返回 `None` 时也返回 `None` 注意 `zip` 只产生4对值理论上第五对值 `(5, None)` 从未被产生,因为 `zip` 在任一输入迭代器返回 `None` 时也返回 `None`

View File

@ -8,7 +8,7 @@
### 使用迭代器并去掉 `clone` ### 使用迭代器并去掉 `clone`
列表 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在列表 13-24 中原原本本的重现了第十二章结尾 `Config::new` 函数的实现: 示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中原原本本的重现了第十二章结尾 `Config::new` 函数的实现:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -29,7 +29,7 @@ impl Config {
} }
``` ```
<span class="caption">列表 13-24重现第十二章结尾的 `Config::new` 函数</span> <span class="caption">示例 13-24重现第十二章结尾的 `Config::new` 函数</span>
这时可以不必担心低效的 `clone` 调用了,因为将来可以去掉他们。好吧,就是现在! 这时可以不必担心低效的 `clone` 调用了,因为将来可以去掉他们。好吧,就是现在!
@ -56,7 +56,7 @@ fn main() {
} }
``` ```
将他们改为如列表 13-25 所示: 将他们改为如示例 13-25 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -71,11 +71,11 @@ fn main() {
} }
``` ```
<span class="caption">列表 13-25`env::args` 的返回值传递给 `Config::new`</span> <span class="caption">示例 13-25`env::args` 的返回值传递给 `Config::new`</span>
`env::args` 函数返回一个迭代器!不同于将迭代器的值收集到一个 vector 中接着传递一个 slice 给 `Config::new`,现在我们直接将 `env::args` 返回的迭代器的所有权传递给 `Config::new` `env::args` 函数返回一个迭代器!不同于将迭代器的值收集到一个 vector 中接着传递一个 slice 给 `Config::new`,现在我们直接将 `env::args` 返回的迭代器的所有权传递给 `Config::new`
接下来需要更新 `Config::new` 的定义。在 I/O 项目的 *src/lib.rs* 中,将 `Config::new` 的签名改为如列表 13-26 所示: 接下来需要更新 `Config::new` 的定义。在 I/O 项目的 *src/lib.rs* 中,将 `Config::new` 的签名改为如示例 13-26 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -85,13 +85,13 @@ impl Config {
// ...snip... // ...snip...
``` ```
<span class="caption">列表 13-26更新 `Config::new` 的签名来接受一个迭代器</span> <span class="caption">示例 13-26更新 `Config::new` 的签名来接受一个迭代器</span>
`env::args` 函数的标准库文档展示了其返回的迭代器类型是 `std::env::Args`。需要更新 `Config::new` 函数的签名中 `args` 参数的类型为 `std::env::Args` 而不是 `&[String]` `env::args` 函数的标准库文档展示了其返回的迭代器类型是 `std::env::Args`。需要更新 `Config::new` 函数的签名中 `args` 参数的类型为 `std::env::Args` 而不是 `&[String]`
#### 使用 `Iterator` trait 方法带起索引 #### 使用 `Iterator` trait 方法带起索引
接下来修复 `Config::new` 的函数体。标准库文档也提到了 `std::env::Args` 实现了 `Iterator` trait所以可以在其上调用 `next` 方法!列表 13-27 更新了列表 12-23 中的代码为使用 `next` 方法: 接下来修复 `Config::new` 的函数体。标准库文档也提到了 `std::env::Args` 实现了 `Iterator` trait所以可以在其上调用 `next` 方法!示例 13-27 更新了示例 12-23 中的代码为使用 `next` 方法:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -127,13 +127,13 @@ impl Config {
} }
``` ```
<span class="caption">列表 13-27修改 `Config::new` 的函数体为使用迭代器方法</span> <span class="caption">示例 13-27修改 `Config::new` 的函数体为使用迭代器方法</span>
请记住 `env::args` 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用 `next` 并不对返回值做任何操作。之后对希望放入 `Config` 中字段 `query` 调用 `next`。如果 `next` 返回 `Some`,使用 `match` 来提取其值。如果它返回 `None`,则意味着没有提供足够的参数并通过 `Err` 值提早返回。对 `filename` 值进行同样的操作。 请记住 `env::args` 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用 `next` 并不对返回值做任何操作。之后对希望放入 `Config` 中字段 `query` 调用 `next`。如果 `next` 返回 `Some`,使用 `match` 来提取其值。如果它返回 `None`,则意味着没有提供足够的参数并通过 `Err` 值提早返回。对 `filename` 值进行同样的操作。
### 使用迭代器适配器来使代码更简明 ### 使用迭代器适配器来使代码更简明
I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,在列表 13-28 中重现了第十二章结尾的此函数定义: I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,在示例 13-28 中重现了第十二章结尾的此函数定义:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -151,9 +151,9 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">列表 13-28第十二章结尾 `search` 函数的定义</span> <span class="caption">示例 13-28第十二章结尾 `search` 函数的定义</span>
可以通过使用迭代器适配器方法来编写更短的代码。这也避免了一个可变的中间 `results` vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 `results` vector 的并发访问。列表 13-29 展示了该变化: 可以通过使用迭代器适配器方法来编写更短的代码。这也避免了一个可变的中间 `results` vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 `results` vector 的并发访问。示例 13-29 展示了该变化:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -165,10 +165,10 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">列表 13-29`search` 函数实现中使用迭代器适配器</span> <span class="caption">示例 13-29`search` 函数实现中使用迭代器适配器</span>
回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于列表 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 为真的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。 回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 为真的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。
接下来的逻辑问题就是在代码中应该选择哪种风格:列表 13-28 中的原始实现,或者是列表 13-29 中使用迭代器的版本。大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector迭代器代码则更关注循环的目的。这抽象出了那些老生常谈的代码这样就更容易看清代码所特有的概念比如迭代器中每个元素必须面对的过滤条件。 接下来的逻辑问题就是在代码中应该选择哪种风格:示例 13-28 中的原始实现,或者是示例 13-29 中使用迭代器的版本。大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector迭代器代码则更关注循环的目的。这抽象出了那些老生常谈的代码这样就更容易看清代码所特有的概念比如迭代器中每个元素必须面对的过滤条件。
不过这两种实现真的完全等同吗?直觉上的假设是更底层的循环会更快一些。让我们聊聊性能吧。 不过这两种实现真的完全等同吗?直觉上的假设是更底层的循环会更快一些。让我们聊聊性能吧。

View File

@ -13,7 +13,7 @@ Rust 和 Cargo 有一些帮助它人更方便找到和使用你发布的包的
准确的包文档有助于其他用户立即如何以及何时使用他们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释***documentation comments*),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的程序员理解如何 **使用** 这个 crate而不是它是如何被 **实现** 的。 准确的包文档有助于其他用户立即如何以及何时使用他们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释***documentation comments*),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的程序员理解如何 **使用** 这个 crate而不是它是如何被 **实现** 的。
文档注释使用 `///` 而不是 `//` 并支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。列表 14-2 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释: 文档注释使用 `///` 而不是 `//` 并支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-2 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -32,7 +32,7 @@ pub fn add_one(x: i32) -> i32 {
} }
``` ```
<span class="caption">列表 14-2一个函数的文档注释</span> <span class="caption">示例 14-2一个函数的文档注释</span>
这里,我们提供了一个 `add_one` 函数工作的描述,接着开始了一个标题为 “Examples” 的部分,和展示如何使用 `add_one` 函数的代码。可以运行 `cargo doc` 来生成这个文档注释的 HTML 文档。这个命令运行由 Rust 分发的工具 `rustdoc` 并将生成的 HTML 文档放入 *target/doc* 目录。 这里,我们提供了一个 `add_one` 函数工作的描述,接着开始了一个标题为 “Examples” 的部分,和展示如何使用 `add_one` 函数的代码。可以运行 `cargo doc` 来生成这个文档注释的 HTML 文档。这个命令运行由 Rust 分发的工具 `rustdoc` 并将生成的 HTML 文档放入 *target/doc* 目录。
@ -44,7 +44,7 @@ pub fn add_one(x: i32) -> i32 {
#### 常用(文档注释)部分 #### 常用(文档注释)部分
列表 14-2 中使用了 `# Examples` Markdown 标题在 HTML 中创建了一个以 “Examples” 为标题的部分。一些其他经常在文档注释中使用的部分有: 示例 14-2 中使用了 `# Examples` Markdown 标题在 HTML 中创建了一个以 “Examples” 为标题的部分。一些其他经常在文档注释中使用的部分有:
- Panics这个函数可能会 `panic!` 的场景。并不希望程序崩溃的函数调用者应该确保他们不会在这些情况下调用此函数。 - Panics这个函数可能会 `panic!` 的场景。并不希望程序崩溃的函数调用者应该确保他们不会在这些情况下调用此函数。
- Errors如果这个函数返回 `Result`,此部分描述可能会出现何种错误以及什么情况会造成这些错误,这有助于调用者编写代码来采用不同的方式处理不同的错误。 - Errors如果这个函数返回 `Result`,此部分描述可能会出现何种错误以及什么情况会造成这些错误,这有助于调用者编写代码来采用不同的方式处理不同的错误。
@ -54,7 +54,7 @@ pub fn add_one(x: i32) -> i32 {
#### 文档注释作为测试 #### 文档注释作为测试
在文档注释中增加示例代码块是一个清楚的表明如何使用库的方法,这么做还有一个额外的好处:`cargo test` 也会像测试那样运行文档中的示例代码!没有什么比有例子的文档更好的了!也没有什么比不能正常工作的例子更糟的了,因为代码在编写文档时已经改变。尝试 `cargo test` 运行像列表 14-2 中 `add_one` 函数的文档;应该在测试结果中看到像这样的部分: 在文档注释中增加示例代码块是一个清楚的表明如何使用库的方法,这么做还有一个额外的好处:`cargo test` 也会像测试那样运行文档中的示例代码!没有什么比有例子的文档更好的了!也没有什么比不能正常工作的例子更糟的了,因为代码在编写文档时已经改变。尝试 `cargo test` 运行像示例 14-2 中 `add_one` 函数的文档;应该在测试结果中看到像这样的部分:
```text ```text
Doc-tests my_crate Doc-tests my_crate
@ -71,7 +71,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
还有另一种风格的文档注释,`//!`,这为包含注释的项,而不是注释之后的项增加文档。这通常用于 crate 根文件或模块的根文件为 crate 或模块整体提供文档。 还有另一种风格的文档注释,`//!`,这为包含注释的项,而不是注释之后的项增加文档。这通常用于 crate 根文件或模块的根文件为 crate 或模块整体提供文档。
作为一个例子,如果我们希望增加描述包含 `add_one` 函数的 `my_crate` crate 目的的文档,可以在 *src/lib.rs* 开头增加以 `//!` 开头的注释,如列表 14-4 所示: 作为一个例子,如果我们希望增加描述包含 `add_one` 函数的 `my_crate` crate 目的的文档,可以在 *src/lib.rs* 开头增加以 `//!` 开头的注释,如示例 14-4 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -85,7 +85,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
// ...snip... // ...snip...
``` ```
<span class="caption">列表 14-4`my_crate` crate 整体的文档</span> <span class="caption">示例 14-4`my_crate` crate 整体的文档</span>
注意 `//!` 的最后一行之后没有任何代码。因为他们以 `//!` 开头而不是 `///`,这是属于包含此注释的项而不是注释之后项的文档。在这个情况中,包含这个注释的项是 *src/lib.rs* 文件,也就是 crate 根文件。这些注释描述了整个 crate。 注意 `//!` 的最后一行之后没有任何代码。因为他们以 `//!` 开头而不是 `///`,这是属于包含此注释的项而不是注释之后项的文档。在这个情况中,包含这个注释的项是 *src/lib.rs* 文件,也就是 crate 根文件。这些注释描述了整个 crate。
@ -106,7 +106,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
好消息是,如果结果对于用户来说 **不是** 很方便,你也无需重新安排内部组织:你可以选择使用 `pub use` 重导出re-export项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置好像它就定义在这个新位置一样。 好消息是,如果结果对于用户来说 **不是** 很方便,你也无需重新安排内部组织:你可以选择使用 `pub use` 重导出re-export项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置好像它就定义在这个新位置一样。
例如,假设我们创建了一个模块化了充满艺术化气息的库 `art`。在这个库中是一个包含两个枚举 `PrimaryColor``SecondaryColor` 的模块 `kinds`,以及一个包含函数 `mix` 的模块 `utils`,如列表 14-6 所示: 例如,假设我们创建了一个模块化了充满艺术化气息的库 `art`。在这个库中是一个包含两个枚举 `PrimaryColor``SecondaryColor` 的模块 `kinds`,以及一个包含函数 `mix` 的模块 `utils`,如示例 14-6 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -143,7 +143,7 @@ pub mod utils {
} }
``` ```
<span class="caption">列表 14-6一个库 `art` 其组织包含 `kinds``utils` 模块</span> <span class="caption">示例 14-6一个库 `art` 其组织包含 `kinds``utils` 模块</span>
`cargo doc` 所生成的 crate 文档首页如图 14-7 所示: `cargo doc` 所生成的 crate 文档首页如图 14-7 所示:
@ -153,7 +153,7 @@ pub mod utils {
注意 `PrimaryColor``SecondaryColor` 类型没有在首页中列出,`mix` 函数也是。必须点击 `kinds``utils` 才能看到他们。 注意 `PrimaryColor``SecondaryColor` 类型没有在首页中列出,`mix` 函数也是。必须点击 `kinds``utils` 才能看到他们。
另一个依赖这个库的 crate 需要 `use` 语句来导入 `art` 中的项,这包含指定其当前定义的模块结构。列表 14-8 展示了一个使用 `art` crate 中 `PrimaryColor``mix` 项的 crate 的例子: 另一个依赖这个库的 crate 需要 `use` 语句来导入 `art` 中的项,这包含指定其当前定义的模块结构。示例 14-8 展示了一个使用 `art` crate 中 `PrimaryColor``mix` 项的 crate 的例子:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -170,11 +170,11 @@ fn main() {
} }
``` ```
<span class="caption">列表 14-8一个通过导出内部结构使用 `art` crate 中项的 crate</span> <span class="caption">示例 14-8一个通过导出内部结构使用 `art` crate 中项的 crate</span>
列表 14-8 中使用 `art` crate 代码的作者不得不搞清楚 `PrimaryColor` 位于 `kinds` 模块而 `mix` 位于 `utils` 模块。`art` crate 的模块结构相比使用它的开发者来说对编写它的开发者更有意义。其内部的 `kinds` 模块和 `utils` 模块的组织结构并没有对尝试理解如何使用它的人提供任何有价值的信息。`art` crate 的模块结构因不得不搞清楚所需的内容在何处和必须在 `use` 语句中指定模块名称而显得混乱和不便。 示例 14-8 中使用 `art` crate 代码的作者不得不搞清楚 `PrimaryColor` 位于 `kinds` 模块而 `mix` 位于 `utils` 模块。`art` crate 的模块结构相比使用它的开发者来说对编写它的开发者更有意义。其内部的 `kinds` 模块和 `utils` 模块的组织结构并没有对尝试理解如何使用它的人提供任何有价值的信息。`art` crate 的模块结构因不得不搞清楚所需的内容在何处和必须在 `use` 语句中指定模块名称而显得混乱和不便。
为了从公有 API 中去掉 crate 的内部组织,我们可以采用列表 14-6 中的 `art` crate 并增加 `pub use` 语句来重导出项到顶层结构,如列表 14-9 所示: 为了从公有 API 中去掉 crate 的内部组织,我们可以采用示例 14-6 中的 `art` crate 并增加 `pub use` 语句来重导出项到顶层结构,如示例 14-9 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -196,7 +196,7 @@ pub mod utils {
} }
``` ```
<span class="caption">列表 14-9增加 `pub use` 语句重导出项</span> <span class="caption">示例 14-9增加 `pub use` 语句重导出项</span>
现在此 crate 由 `cargo doc` 生成的 API 文档会在首页列出重导出的项以及其链接,如图 14-10 所示,这就使得这些类型易于查找。 现在此 crate 由 `cargo doc` 生成的 API 文档会在首页列出重导出的项以及其链接,如图 14-10 所示,这就使得这些类型易于查找。
@ -204,7 +204,7 @@ pub mod utils {
<span class="caption">图 14-10`art` 文档的首页,这里列出了重导出的项</span> <span class="caption">图 14-10`art` 文档的首页,这里列出了重导出的项</span>
`art` crate 的用户仍然可以看见和选择使用列表 14-8 中的内部结构,或者可以使用列表 14-9 中更为方便的结构,如列表 14-11 所示: `art` crate 的用户仍然可以看见和选择使用示例 14-8 中的内部结构,或者可以使用示例 14-9 中更为方便的结构,如示例 14-11 所示:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -219,7 +219,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 14-11一个使用 `art` crate</span> <span class="caption">示例 14-11一个使用 `art` crate</span>
对于有很多嵌套模块的情况,使用 `pub use` 将类型重导出到顶级结构对于使用 crate 的人来说将会是大为不同的体验。 对于有很多嵌套模块的情况,使用 `pub use` 将类型重导出到顶级结构对于使用 crate 的人来说将会是大为不同的体验。

View File

@ -65,7 +65,7 @@ pub fn add_one(x: i32) -> i32 {
} }
``` ```
打开 `adder`*src/main.rs* 并增加一行 `extern crate` 将新的 `add-one` 库引入作用域,并修改 `main` 函数来调用 `add_one` 函数,如列表 14-12 所示: 打开 `adder`*src/main.rs* 并增加一行 `extern crate` 将新的 `add-one` 库引入作用域,并修改 `main` 函数来调用 `add_one` 函数,如示例 14-12 所示:
```rust,ignore ```rust,ignore
extern crate add_one; extern crate add_one;
@ -76,7 +76,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 14-12使用来自 `adder` crate 的库 crate `add-one`</span> <span class="caption">示例 14-12使用来自 `adder` crate 的库 crate `add-one`</span>
*adder* 目录下运行 `cargo build` 来构建 `adder` crate *adder* 目录下运行 `cargo build` 来构建 `adder` crate

View File

@ -4,7 +4,7 @@
> <br> > <br>
> commit 348d78235faa10375ce5a3554e2c34d3275c174f > commit 348d78235faa10375ce5a3554e2c34d3275c174f
最简单直接的智能指针是 *box*,它的类型是 `Box<T>`。 box 允许你将一个值放在堆上(第四章介绍过栈与堆)。列表 15-1 展示了如何使用 box 在堆上储存一个`i32` 最简单直接的智能指针是 *box*,它的类型是 `Box<T>`。 box 允许你将一个值放在堆上(第四章介绍过栈与堆)。示例 15-1 展示了如何使用 box 在堆上储存一个`i32`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -15,11 +15,11 @@ fn main() {
} }
``` ```
<span class="caption">列表 15-1使用 box 在堆上储存一个 `i32`</span> <span class="caption">示例 15-1使用 box 在堆上储存一个 `i32`</span>
这会打印出 `b = 5`。在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 `b` 这样的 box 在 `main` 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。 这会打印出 `b = 5`。在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 `b` 这样的 box 在 `main` 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
将一个单独的值存放在堆上并不是很有意义,所以像列表 15-1 这样单独使用 box 并不常见。一个 box 的实用场景是当你希望确保类型有一个已知大小的时候。例如,考虑一下列表 15-2它是一个用于 *cons list* 的枚举定义,这是一个来源于函数式编程的数据结构类型。注意它还不能编译: 将一个单独的值存放在堆上并不是很有意义,所以像示例 15-1 这样单独使用 box 并不常见。一个 box 的实用场景是当你希望确保类型有一个已知大小的时候。例如,考虑一下示例 15-2它是一个用于 *cons list* 的枚举定义,这是一个来源于函数式编程的数据结构类型。注意它还不能编译:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -30,7 +30,7 @@ enum List {
} }
``` ```
<span class="caption">列表 15-2第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举</span> <span class="caption">示例 15-2第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举</span>
我们实现了一个只存放 `i32` 值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。 我们实现了一个只存放 `i32` 值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。
@ -56,7 +56,7 @@ fn main() {
第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2``Cons` 值和下一个 `List` 值。这又是另一个存放了 `3``Cons` 值和最后一个值为 `Nil``List`,非递归成员代表了列表的结尾。 第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2``Cons` 值和下一个 `List` 值。这又是另一个存放了 `3``Cons` 值和最后一个值为 `Nil``List`,非递归成员代表了列表的结尾。
如果尝试编译上面的代码,会得到如列表 15-3 所示的错误: 如果尝试编译上面的代码,会得到如示例 15-3 所示的错误:
```text ```text
error[E0072]: recursive type `List` has infinite size error[E0072]: recursive type `List` has infinite size
@ -71,9 +71,9 @@ error[E0072]: recursive type `List` has infinite size
make `List` representable make `List` representable
``` ```
<span class="caption">列表 15-3尝试定义一个递归枚举时得到的错误</span> <span class="caption">示例 15-3尝试定义一个递归枚举时得到的错误</span>
这个错误表明这个类型 “有无限的大小”。为什么呢?因为 `List` 的一个成员被定义为是递归的:它存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们一点一点来看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。回忆一下第六章讨论枚举定义时的列表 6-2 中定义的 `Message` 枚举: 这个错误表明这个类型 “有无限的大小”。为什么呢?因为 `List` 的一个成员被定义为是递归的:它存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们一点一点来看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。回忆一下第六章讨论枚举定义时的示例 6-2 中定义的 `Message` 枚举:
```rust ```rust
enum Message { enum Message {
@ -86,20 +86,20 @@ enum Message {
当 Rust 需要知道需要为 `Message` 值分配多少空间时,它可以检查每一个成员并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因此,`Message` 值所需的空间等于储存其最大成员的空间大小。 当 Rust 需要知道需要为 `Message` 值分配多少空间时,它可以检查每一个成员并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因此,`Message` 值所需的空间等于储存其最大成员的空间大小。
与此相对当 Rust 编译器检查像列表 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-4 所示: 与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-4 所示:
<img alt="An infinite Cons list" src="img/trpl15-01.svg" class="center" style="width: 50%;" /> <img alt="An infinite Cons list" src="img/trpl15-01.svg" class="center" style="width: 50%;" />
<span class="caption">图 15-4一个包含无限个 `Cons` 成员的无限 `List`</span> <span class="caption">图 15-4一个包含无限个 `Cons` 成员的无限 `List`</span>
Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了列表 15-3 中的错误。这个错误也包括了有用的建议: Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了示例 15-3 中的错误。这个错误也包括了有用的建议:
```text ```text
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable make `List` representable
``` ```
因为 `Box<T>` 是一个指针,我们总是知道它需要多少空间:指针需要一个 `usize` 大小的空间。这个 `usize` 的值将是堆数据的地址。而堆数据可以是任意大小,不过这个堆数据开头的地址总是能放进一个 `usize` 中。我们可以将列表 15-2 的定义修改为像这里列表 15-5 中的定义,并修改 `main` 函数对 `Cons` 成员中的值使用 `Box::new` 因为 `Box<T>` 是一个指针,我们总是知道它需要多少空间:指针需要一个 `usize` 大小的空间。这个 `usize` 的值将是堆数据的地址。而堆数据可以是任意大小,不过这个堆数据开头的地址总是能放进一个 `usize` 中。我们可以将示例 15-2 的定义修改为像这里示例 15-5 中的定义,并修改 `main` 函数对 `Cons` 成员中的值使用 `Box::new`
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -119,7 +119,7 @@ fn main() {
} }
``` ```
<span class="caption">列表 15-5为了已知大小使用 `Box<T>``List` 定义</span> <span class="caption">示例 15-5为了已知大小使用 `Box<T>``List` 定义</span>
这样编译器就能够计算出储存一个 `List` 值需要的大小了。Rust 将会检查 `List`,同样的从 `Cons` 成员开始检查。`Cons` 成员需要 `i32` 的大小加上一个 `usize` 的大小,因为 box 总是 `usize` 大小的,不管它指向的是什么。接着 Rust 检查 `Nil` 成员,它并不储存一个值,所以 `Nil` 并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在 `Cons` 成员看起来像什么: 这样编译器就能够计算出储存一个 `List` 值需要的大小了。Rust 将会检查 `List`,同样的从 `Cons` 成员开始检查。`Cons` 成员需要 `i32` 的大小加上一个 `usize` 的大小,因为 box 总是 `usize` 大小的,不管它指向的是什么。接着 Rust 检查 `Nil` 成员,它并不储存一个值,所以 `Nil` 并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在 `Cons` 成员看起来像什么:

View File

@ -23,7 +23,7 @@ assert_eq!(6, x);
引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据,因为解引用一个常规的引用只能给我们数据而不是元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现 `Deref` trait 来重载 `*` 运算符的行为。 引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据,因为解引用一个常规的引用只能给我们数据而不是元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现 `Deref` trait 来重载 `*` 运算符的行为。
列表 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过 `Deref` trait 来重载 `*` 的例子。`Mp3`,在某种意义上是一个智能指针:它拥有包含音频的 `Vec<u8>` 数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现 `Deref` trait 来返回音频数据。实现 `Deref` trait 需要一个叫做 `deref` 的方法,它借用 `self` 并返回其内部数据: 示例 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过 `Deref` trait 来重载 `*` 的例子。`Mp3`,在某种意义上是一个智能指针:它拥有包含音频的 `Vec<u8>` 数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现 `Deref` trait 来返回音频数据。实现 `Deref` trait 需要一个叫做 `deref` 的方法,它借用 `self` 并返回其内部数据:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -56,13 +56,13 @@ fn main() {
} }
``` ```
<span class="caption">列表 15-7一个存放 mp3 文件数据和元数据的结构体上的 `Deref` trait 实现</span> <span class="caption">示例 15-7一个存放 mp3 文件数据和元数据的结构体上的 `Deref` trait 实现</span>
大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体实例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的 `type Item``type Target = T;` 语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。 大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体实例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的 `type Item``type Target = T;` 语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。
`assert_eq!` 中,我们验证 `vec![1, 2, 3]` 是否为 `Mp3` 实例 `*my_favorite_song` 解引用的值,结果正是如此,因为我们实现了 `deref` 方法来返回音频数据。如果没有为 `Mp3` 实现 `Deref` traitRust 将不会编译 `*my_favorite_song`:会出现错误说 `Mp3` 类型不能被解引用。 `assert_eq!` 中,我们验证 `vec![1, 2, 3]` 是否为 `Mp3` 实例 `*my_favorite_song` 解引用的值,结果正是如此,因为我们实现了 `deref` 方法来返回音频数据。如果没有为 `Mp3` 实现 `Deref` traitRust 将不会编译 `*my_favorite_song`:会出现错误说 `Mp3` 类型不能被解引用。
没有 `Deref` trait 的话,编译器只能解引用 `&` 引用,而 `my_favorite_song` 并不是(它是一个 `Mp3` 结构体)。通过 `Deref` trait编译器知道实现了 `Deref` trait 的类型有一个返回引用的 `deref` 方法(在这个例子中,是 `&self.audio` 因为列表 15-7 中的 `deref` 的定义)。所以为了得到一个 `*` 可以解引用的 `&` 引用,编译器将 `*my_favorite_song` 展开为如下: 没有 `Deref` trait 的话,编译器只能解引用 `&` 引用,而 `my_favorite_song` 并不是(它是一个 `Mp3` 结构体)。通过 `Deref` trait编译器知道实现了 `Deref` trait 的类型有一个返回引用的 `deref` 方法(在这个例子中,是 `&self.audio` 因为示例 15-7 中的 `deref` 的定义)。所以为了得到一个 `*` 可以解引用的 `&` 引用,编译器将 `*my_favorite_song` 展开为如下:
```rust,ignore ```rust,ignore
*(my_favorite_song.deref()) *(my_favorite_song.deref())
@ -70,13 +70,13 @@ fn main() {
其结果就是 `self.audio` 中的值。`deref` 返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果 `deref` 方法直接返回值而不是引用,其值将被移动出 `self`。和大部分使用解引用运算符的地方相同,这里并不想获取 `my_favorite_song.audio` 的所有权。 其结果就是 `self.audio` 中的值。`deref` 返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果 `deref` 方法直接返回值而不是引用,其值将被移动出 `self`。和大部分使用解引用运算符的地方相同,这里并不想获取 `my_favorite_song.audio` 的所有权。
注意将 `*` 替换为 `deref` 调用和 `*` 调用的过程在每次使用 `*` 的时候都会发生一次。`*` 的替换并不会无限递归进行。最终的数据类型是 `Vec<u8>`,它与列表 15-7 中 `assert_eq!``vec![1, 2, 3]` 相匹配。 注意将 `*` 替换为 `deref` 调用和 `*` 调用的过程在每次使用 `*` 的时候都会发生一次。`*` 的替换并不会无限递归进行。最终的数据类型是 `Vec<u8>`,它与示例 15-7 中 `assert_eq!``vec![1, 2, 3]` 相匹配。
### 函数和方法的隐式解引用强制多态 ### 函数和方法的隐式解引用强制多态
Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的 **解引用强制多态***deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多显式使用 `&``*` 的引用和解引用。 Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的 **解引用强制多态***deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多显式使用 `&``*` 的引用和解引用。
使用列表 15-7 中的 `Mp3` 结构体,如下是一个获取 `u8` slice 并压缩 mp3 音频数据的函数签名: 使用示例 15-7 中的 `Mp3` 结构体,如下是一个获取 `u8` slice 并压缩 mp3 音频数据的函数签名:
```rust,ignore ```rust,ignore
fn compress_mp3(audio: &[u8]) -> Vec<u8> { fn compress_mp3(audio: &[u8]) -> Vec<u8> {

View File

@ -10,7 +10,7 @@
指定在值离开作用域时应该执行的代码的方式是实现`Drop` trait。`Drop` trait 要求我们实现一个叫做`drop`的方法,它获取一个`self`的可变引用。 指定在值离开作用域时应该执行的代码的方式是实现`Drop` trait。`Drop` trait 要求我们实现一个叫做`drop`的方法,它获取一个`self`的可变引用。
列表 15-8 展示了并没有实际功能的结构体`CustomSmartPointer`,不过我们会在创建实例之后打印出`CustomSmartPointer created.`,而在实例离开作用域时打印出`Dropping CustomSmartPointer!`,这样就能看出每一段代码是何时被执行的。实际的项目中,我们应该在`drop`中清理任何智能指针运行所需要的资源,而不是这个例子中的`println!`语句: 示例 15-8 展示了并没有实际功能的结构体`CustomSmartPointer`,不过我们会在创建实例之后打印出`CustomSmartPointer created.`,而在实例离开作用域时打印出`Dropping CustomSmartPointer!`,这样就能看出每一段代码是何时被执行的。实际的项目中,我们应该在`drop`中清理任何智能指针运行所需要的资源,而不是这个例子中的`println!`语句:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -48,7 +48,7 @@ Dropping CustomSmartPointer!
被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了`drop`。 被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了`drop`。
可以使用`std::mem::drop`函数来在值离开作用域之前丢弃它。这通常是不必要的;整个`Drop` trait 的要点在于它自动的帮我们处理清理工作。在第十六章讲到并发时我们会看到一个需要在离开作用域之前丢弃值的例子。现在知道这是可能的即可,`std::mem::drop`位于 prelude 中所以可以如列表 15-9 所示直接调用`drop` 可以使用`std::mem::drop`函数来在值离开作用域之前丢弃它。这通常是不必要的;整个`Drop` trait 的要点在于它自动的帮我们处理清理工作。在第十六章讲到并发时我们会看到一个需要在离开作用域之前丢弃值的例子。现在知道这是可能的即可,`std::mem::drop`位于 prelude 中所以可以如示例 15-9 所示直接调用`drop`
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -72,7 +72,7 @@ Dropping CustomSmartPointer!
Wait for it... Wait for it...
``` ```
注意不允许直接调用我们定义的`drop`方法:如果将列表 15-9 中的`drop(c)`替换为`c.drop()`,会得到一个编译错误表明`explicit destructor calls not allowed`。不允许直接调用`Drop::drop`的原因是 Rust 在值离开作用域时会自动插入`Drop::drop`,这样就会丢弃值两次。丢弃一个值两次可能会造成错误或破坏内存,所以 Rust 就不允许这么做。相应的可以调用`std::mem::drop`,它的定义是: 注意不允许直接调用我们定义的`drop`方法:如果将示例 15-9 中的`drop(c)`替换为`c.drop()`,会得到一个编译错误表明`explicit destructor calls not allowed`。不允许直接调用`Drop::drop`的原因是 Rust 在值离开作用域时会自动插入`Drop::drop`,这样就会丢弃值两次。丢弃一个值两次可能会造成错误或破坏内存,所以 Rust 就不允许这么做。相应的可以调用`std::mem::drop`,它的定义是:
```rust ```rust
pub mod std { pub mod std {

View File

@ -14,14 +14,14 @@
### 使用`Rc<T>`分享数据 ### 使用`Rc<T>`分享数据
让我们回到列表 15-5 中的 cons list 例子。在列表 15-11 中尝试使用`Box<T>`定义的`List`。首先创建了一个包含 5 接着是 10 的列表实例。之后我们想要创建另外两个列表:一个以 3 开始并后接第一个包含 5 和 10 的列表,另一个以 4 开始其后**也**是第一个列表。换句话说,我们希望这两个列表共享第三个列表的所有权,概念上类似于图 15-10 让我们回到示例 15-5 中的 cons list 例子。在示例 15-11 中尝试使用`Box<T>`定义的`List`。首先创建了一个包含 5 接着是 10 的列表实例。之后我们想要创建另外两个列表:一个以 3 开始并后接第一个包含 5 和 10 的列表,另一个以 4 开始其后**也**是第一个列表。换句话说,我们希望这两个列表共享第三个列表的所有权,概念上类似于图 15-10
<img alt="Two lists that share ownership of a third list" src="img/trpl15-03.svg" class="center" /> <img alt="Two lists that share ownership of a third list" src="img/trpl15-03.svg" class="center" />
<span class="caption">Figure 15-10: Two lists, `b` and `c`, sharing ownership <span class="caption">Figure 15-10: Two lists, `b` and `c`, sharing ownership
of a third list, `a`</span> of a third list, `a`</span>
尝试使用`Box<T>`定义的`List`并不能工作,如列表 15-11 所示: 尝试使用`Box<T>`定义的`List`并不能工作,如示例 15-11 所示:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -64,7 +64,7 @@ error[E0382]: use of moved value: `a`
相反可以改变`Cons`的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。 相反可以改变`Cons`的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。
列表 15-12 所示,可以将`List`的定义从`Box<T>`改为`Rc<T>` 示例 15-12 所示,可以将`List`的定义从`Box<T>`改为`Rc<T>`
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -91,7 +91,7 @@ fn main() {
### 克隆`Rc<T>`会增加引用计数 ### 克隆`Rc<T>`会增加引用计数
之前我们见过`clone`方法,当时使用它来创建某些数据的完整拷贝。但是对于`Rc<T>`来说,它并不创建一个完整的拷贝。`Rc<T>`存放了**引用计数**,也就是说,一个存在多少个克隆的计数器。让我们像列表 15-13 那样在创建`c`时增加一个内部作用域,并在不同的位置打印出关联函数`Rc::strong_count`的结果。`Rc::strong_count`返回传递给它的`Rc`值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做`strong_count`。 之前我们见过`clone`方法,当时使用它来创建某些数据的完整拷贝。但是对于`Rc<T>`来说,它并不创建一个完整的拷贝。`Rc<T>`存放了**引用计数**,也就是说,一个存在多少个克隆的计数器。让我们像示例 15-13 那样在创建`c`时增加一个内部作用域,并在不同的位置打印出关联函数`Rc::strong_count`的结果。`Rc::strong_count`返回传递给它的`Rc`值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做`strong_count`。
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>

View File

@ -27,7 +27,7 @@ Rust 编译器执行的静态分析天生是保守的。代码的一些属性则
对于引用,可以使用`&`和`&mut`语法来分别创建不可变和可变的引用。不过对于`RefCell<T>`,我们使用`borrow`和`borrow_mut`方法,它是`RefCell<T>`拥有的安全 API 的一部分。`borrow`返回`Ref`类型的智能指针,而`borrow_mut`返回`RefMut`类型的智能指针。这两个类型实现了`Deref`所以可以被当作常规引用处理。`Ref`和`RefMut`动态的借用所有权,而他们的`Drop`实现也动态的释放借用。 对于引用,可以使用`&`和`&mut`语法来分别创建不可变和可变的引用。不过对于`RefCell<T>`,我们使用`borrow`和`borrow_mut`方法,它是`RefCell<T>`拥有的安全 API 的一部分。`borrow`返回`Ref`类型的智能指针,而`borrow_mut`返回`RefMut`类型的智能指针。这两个类型实现了`Deref`所以可以被当作常规引用处理。`Ref`和`RefMut`动态的借用所有权,而他们的`Drop`实现也动态的释放借用。
列表 15-14 展示了如何使用`RefCell<T>`来使函数不可变的和可变的借用它的参数。注意`data`变量使用`let data`而不是`let mut data`来声明为不可变的,而`a_fn_that_mutably_borrows`则允许可变的借用数据并修改它! 示例 15-14 展示了如何使用`RefCell<T>`来使函数不可变的和可变的借用它的参数。注意`data`变量使用`let data`而不是`let mut data`来声明为不可变的,而`a_fn_that_mutably_borrows`则允许可变的借用数据并修改它!
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -120,7 +120,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
### 结合`Rc<T>`和`RefCell<T>`来拥有多个可变数据所有者 ### 结合`Rc<T>`和`RefCell<T>`来拥有多个可变数据所有者
那么为什么要权衡考虑选择引入`RefCell<T>`呢?好吧,还记得我们说过`Rc<T>`只能拥有一个`T`的不可变引用吗?考虑到`RefCell<T>`是不可变的,但是拥有内部可变性,可以将`Rc<T>`与`RefCell<T>`结合来创造一个既有引用计数又可变的类型。列表 15-15 展示了一个这么做的例子,再次回到列表 15-5 中的 cons list。在这个例子中不同于在 cons list 中储存`i32`值,我们储存一个`Rc<RefCell<i32>>`值。希望储存这个类型是因为其可以拥有不属于列表一部分的这个值的所有者(`Rc<T>`提供的多个所有者功能),而且还可以改变内部的`i32`值(`RefCell<T>`提供的内部可变性功能): 那么为什么要权衡考虑选择引入`RefCell<T>`呢?好吧,还记得我们说过`Rc<T>`只能拥有一个`T`的不可变引用吗?考虑到`RefCell<T>`是不可变的,但是拥有内部可变性,可以将`Rc<T>`与`RefCell<T>`结合来创造一个既有引用计数又可变的类型。示例 15-15 展示了一个这么做的例子,再次回到示例 15-5 中的 cons list。在这个例子中不同于在 cons list 中储存`i32`值,我们储存一个`Rc<RefCell<i32>>`值。希望储存这个类型是因为其可以拥有不属于列表一部分的这个值的所有者(`Rc<T>`提供的多个所有者功能),而且还可以改变内部的`i32`值(`RefCell<T>`提供的内部可变性功能):
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -155,7 +155,7 @@ fn main() {
<span class="caption">Listing 15-15: Using `Rc<RefCell<i32>>` to create a <span class="caption">Listing 15-15: Using `Rc<RefCell<i32>>` to create a
`List` that we can mutate</span> `List` that we can mutate</span>
我们创建了一个值,它是`Rc<RefCell<i32>>`的实例。将其储存在变量`value`中因为我们希望之后能直接访问它。接着在`a`中创建了一个拥有存放了`value`值的`Cons`成员的`List`,而且`value`需要被克隆因为我们希望除了`a`之外还拥有`value`的所有权。接着将`a`封装进`Rc<T>`中这样就可以创建都引用`a`的有着不同开头的列表`b`和`c`,类似列表 15-12 中所做的那样。 我们创建了一个值,它是`Rc<RefCell<i32>>`的实例。将其储存在变量`value`中因为我们希望之后能直接访问它。接着在`a`中创建了一个拥有存放了`value`值的`Cons`成员的`List`,而且`value`需要被克隆因为我们希望除了`a`之外还拥有`value`的所有权。接着将`a`封装进`Rc<T>`中这样就可以创建都引用`a`的有着不同开头的列表`b`和`c`,类似示例 15-12 中所做的那样。
一旦创建了`shared_list`、`b`和`c`,接下来就可以通过解引用`Rc<T>`和对`RefCell`调用`borrow_mut`来将 10 与 5 相加了。 一旦创建了`shared_list`、`b`和`c`,接下来就可以通过解引用`Rc<T>`和对`RefCell`调用`borrow_mut`来将 10 与 5 相加了。

View File

@ -8,7 +8,7 @@
在使用`Rc<T>`和`RefCell<T>`时,有可能创建循环引用,这时各个项相互引用并形成环。这是不好的因为每一项的引用计数将永远也到不了 0其值也永远也不会被丢弃。让我们看看这是如何发生的以及如何避免它。 在使用`Rc<T>`和`RefCell<T>`时,有可能创建循环引用,这时各个项相互引用并形成环。这是不好的因为每一项的引用计数将永远也到不了 0其值也永远也不会被丢弃。让我们看看这是如何发生的以及如何避免它。
列表 15-16 中,我们将使用列表 15-5 中`List`定义的另一个变体。我们将回到储存`i32`值作为`Cons`成员的第一个元素。现在`Cons`成员的第二个元素是`RefCell<Rc<List>>`:这时就不能修改`i32`值了,但是能够修改`Cons`成员指向的那个`List`。还需要增加一个`tail`方法来方便我们在拥有一个`Cons`成员时访问第二个项: 示例 15-16 中,我们将使用示例 15-5 中`List`定义的另一个变体。我们将回到储存`i32`值作为`Cons`成员的第一个元素。现在`Cons`成员的第二个元素是`RefCell<Rc<List>>`:这时就不能修改`i32`值了,但是能够修改`Cons`成员指向的那个`List`。还需要增加一个`tail`方法来方便我们在拥有一个`Cons`成员时访问第二个项:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -32,7 +32,7 @@ impl List {
<span class="caption">Listing 15-16: A cons list definition that holds a <span class="caption">Listing 15-16: A cons list definition that holds a
`RefCell` so that we can modify what a `Cons` variant is referring to</span> `RefCell` so that we can modify what a `Cons` variant is referring to</span>
接下来,在列表 15-17 中,我们将在变量`a`中创建一个`List`值,其内部是一个`5, Nil`的列表。接着在变量`b`创建一个值 10 和指向`a`中列表的`List`值。最后修改`a`指向`b`而不是`Nil`,这会创建一个循环: 接下来,在示例 15-17 中,我们将在变量`a`中创建一个`List`值,其内部是一个`5, Nil`的列表。接着在变量`b`创建一个值 10 和指向`a`中列表的`List`值。最后修改`a`指向`b`而不是`Nil`,这会创建一个循环:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -96,7 +96,7 @@ pointing to each other</span>
观察最后一个`println!`之前的打印结果,就会发现在将`a`改变为指向`b`之后`a`和`b`的引用计数都是 2。在`main`的结尾Rust 首先会尝试丢弃`b`,这会使`Rc`的引用计数减一,但是这个计数是 1 而不是 0所以`Rc`在堆上的内存不会被丢弃。它只是会永远的停留在 1 上。这个特定例子中,程序立马就结束了,所以并不是一个问题,不过如果是一个更加复杂的程序,它在这个循环中分配了很多内存并占有很长时间,这就是个问题了。这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。 观察最后一个`println!`之前的打印结果,就会发现在将`a`改变为指向`b`之后`a`和`b`的引用计数都是 2。在`main`的结尾Rust 首先会尝试丢弃`b`,这会使`Rc`的引用计数减一,但是这个计数是 1 而不是 0所以`Rc`在堆上的内存不会被丢弃。它只是会永远的停留在 1 上。这个特定例子中,程序立马就结束了,所以并不是一个问题,不过如果是一个更加复杂的程序,它在这个循环中分配了很多内存并占有很长时间,这就是个问题了。这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。
现在,如你所见,在 Rust 中创建引用循环是困难和繁琐的。但并不是不可能:避免引用循环这种形式的内存泄漏并不是 Rust 的保证之一。如果你有包含`Rc<T>`的`RefCell<T>`值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环。在列表 15-14 的例子中,可能解决方式就是不要编写像这样可能造成引用循环的代码,因为我们希望`Cons`成员拥有他们指向的列表。 现在,如你所见,在 Rust 中创建引用循环是困难和繁琐的。但并不是不可能:避免引用循环这种形式的内存泄漏并不是 Rust 的保证之一。如果你有包含`Rc<T>`的`RefCell<T>`值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环。在示例 15-14 的例子中,可能解决方式就是不要编写像这样可能造成引用循环的代码,因为我们希望`Cons`成员拥有他们指向的列表。
举例来说,对于像图这样的数据结构,为了创建父节点指向子节点的边和以相反方向从子节点指向父节点的边,有时需要创建这样的引用循环。如果一个方向拥有所有权而另一个方向没有,对于模拟这种数据关系的一种不会创建引用循环和内存泄露的方式是使用`Weak<T>`。接下来让我们探索一下! 举例来说,对于像图这样的数据结构,为了创建父节点指向子节点的边和以相反方向从子节点指向父节点的边,有时需要创建这样的引用循环。如果一个方向拥有所有权而另一个方向没有,对于模拟这种数据关系的一种不会创建引用循环和内存泄露的方式是使用`Weak<T>`。接下来让我们探索一下!
@ -104,7 +104,7 @@ pointing to each other</span>
Rust 标准库中提供了`Weak<T>`,一个用于存在引用循环但只有一个方向有所有权的智能指针。我们已经展示过如何克隆`Rc<T>`来增加引用的`strong_count``Weak<T>`是一种引用`Rc<T>`但不增加`strong_count`的方式:相反它增加`Rc`引用的`weak_count`。当`Rc`离开作用域,其内部值会在`strong_count`为 0 的时候被丢弃,即便`weak_count`不为 0 。为了能够从`Weak<T>`中获取值,首先需要使用`upgrade`方法将其升级为`Option<Rc<T>>`。升级`Weak<T>`的结果在`Rc`还未被丢弃时是`Some`,而在`Rc`被丢弃时是`None`。因为`upgrade`返回一个`Option`,我们知道 Rust 会确保`Some`和`None`的情况都被处理并不会尝试使用一个无效的指针。 Rust 标准库中提供了`Weak<T>`,一个用于存在引用循环但只有一个方向有所有权的智能指针。我们已经展示过如何克隆`Rc<T>`来增加引用的`strong_count``Weak<T>`是一种引用`Rc<T>`但不增加`strong_count`的方式:相反它增加`Rc`引用的`weak_count`。当`Rc`离开作用域,其内部值会在`strong_count`为 0 的时候被丢弃,即便`weak_count`不为 0 。为了能够从`Weak<T>`中获取值,首先需要使用`upgrade`方法将其升级为`Option<Rc<T>>`。升级`Weak<T>`的结果在`Rc`还未被丢弃时是`Some`,而在`Rc`被丢弃时是`None`。因为`upgrade`返回一个`Option`,我们知道 Rust 会确保`Some`和`None`的情况都被处理并不会尝试使用一个无效的指针。
不同于列表 15-17 中每个项只知道它的下一项,假如我们需要一个树,它的项知道它的子项**和**父项。 不同于示例 15-17 中每个项只知道它的下一项,假如我们需要一个树,它的项知道它的子项**和**父项。
让我们从一个叫做`Node`的存放拥有所有权的`i32`值和其子`Node`值的引用的结构体开始: 让我们从一个叫做`Node`的存放拥有所有权的`i32`值和其子`Node`值的引用的结构体开始:
@ -119,7 +119,7 @@ struct Node {
} }
``` ```
我们希望能够`Node`拥有其子节点,同时也希望变量可以拥有每个节点以便可以直接访问他们。这就是为什么`Vec`中的项是`Rc<Node>`值。我们也希望能够修改其他节点的子节点,这就是为什么`children`中`Vec`被放进了`RefCell`的原因。在列表 15-19 中创建了一个叫做`leaf`的带有值 3 并没有子节点的`Node`实例,和另一个带有值 5 和以`leaf`作为子节点的实例`branch` 我们希望能够`Node`拥有其子节点,同时也希望变量可以拥有每个节点以便可以直接访问他们。这就是为什么`Vec`中的项是`Rc<Node>`值。我们也希望能够修改其他节点的子节点,这就是为什么`children`中`Vec`被放进了`RefCell`的原因。在示例 15-19 中创建了一个叫做`leaf`的带有值 3 并没有子节点的`Node`实例,和另一个带有值 5 和以`leaf`作为子节点的实例`branch`
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -162,7 +162,7 @@ struct Node {
} }
``` ```
这样,一个节点就能够在拥有父节点时指向它,而并不拥有其父节点。一个父节点哪怕在拥有指向它的子节点也会被丢弃,只要是其自身也没有一个父节点就行。现在将`main`函数更新为如列表 15-20 所示: 这样,一个节点就能够在拥有父节点时指向它,而并不拥有其父节点。一个父节点哪怕在拥有指向它的子节点也会被丢弃,只要是其自身也没有一个父节点就行。现在将`main`函数更新为如示例 15-20 所示:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -199,7 +199,7 @@ leaf parent = None
类似的,`branch`也有一个新的`Weak`引用,因为也没有父节点。`leaf`仍然作为`branch`的一个子节点。一旦在`branch`中有了一个新的`Node`实例,就可以修改`leaf`将一个`branch`的`Weak`引用作为其父节点。这里使用了`leaf`中`parent`字段里的`RefCell`的`borrow_mut`方法,接着使用了`Rc::downgrade`函数来从`branch`中的`Rc`值创建了一个指向`branch`的`Weak`引用。 类似的,`branch`也有一个新的`Weak`引用,因为也没有父节点。`leaf`仍然作为`branch`的一个子节点。一旦在`branch`中有了一个新的`Node`实例,就可以修改`leaf`将一个`branch`的`Weak`引用作为其父节点。这里使用了`leaf`中`parent`字段里的`RefCell`的`borrow_mut`方法,接着使用了`Rc::downgrade`函数来从`branch`中的`Rc`值创建了一个指向`branch`的`Weak`引用。
当再次打印出`leaf`的父节点时,这一次将会得到存放了`branch`的`Some`值。另外需要注意到这里并没有打印出类似列表 15-14 中那样最终导致栈溢出的循环:`Weak`引用仅仅打印出`(Weak)` 当再次打印出`leaf`的父节点时,这一次将会得到存放了`branch`的`Some`值。另外需要注意到这里并没有打印出类似示例 15-14 中那样最终导致栈溢出的循环:`Weak`引用仅仅打印出`(Weak)`
``` ```
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
@ -207,7 +207,7 @@ children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } }) children: RefCell { value: [] } }] } })
``` ```
没有无限的输出(或直到栈溢出)的事实表明这里并没有引用循环。另一种证明的方式时观察调用`Rc::strong_count`和`Rc::weak_count`的值。在列表 15-21 中,创建了一个新的内部作用域并将`branch`的创建放入其中,这样可以观察`branch`被创建时和离开作用域被丢弃时发生了什么: 没有无限的输出(或直到栈溢出)的事实表明这里并没有引用循环。另一种证明的方式时观察调用`Rc::strong_count`和`Rc::weak_count`的值。在示例 15-21 中,创建了一个新的内部作用域并将`branch`的创建放入其中,这样可以观察`branch`被创建时和离开作用域被丢弃时发生了什么:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>