check to ch10-01

pull/274/head
KaiserY 6 years ago
parent 8ebec19624
commit 257b316a9c

@ -500,4 +500,202 @@ fn function2() -> IoResult<()> {
}
```
<span class="caption">示例 7-20: 通过 `as` 关键字重命名引入作用域的类型</span>
<span class="caption">示例 7-20: 通过 `as` 关键字重命名引入作用域的类型</span>
在第二个 `use` 语句中,我们选择 `IoResult` 作为 `std::io::Result` 的新名称,它与从 `std::fmt` 引入作用域的 `Result` 并不冲突。这样做也被认为是惯用的;示例 7-19 还是示例 7-20 全看你的选择。
### 通过 `pub use` 重导出名称
当使用 `use` 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果希望调用你编写的代码的代码能够像你一样在其自己的作用域内引用这些类型,可以结合 `pub``use`。这个技术被称为 “重导出”(*re-exporting*),因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。
例如,示例 7-21 展示了示例 7-15 中的代码将 `performance_group``use` 变为 `pub use` 的版本。
<span class="filename">文件名: src/main.rs</span>
```rust
mod sound {
pub mod instrument {
pub fn clarinet() {
// 函数体
}
}
}
mod performance_group {
pub use crate::sound::instrument;
pub fn clarinet_trio() {
instrument::clarinet();
instrument::clarinet();
instrument::clarinet();
}
}
fn main() {
performance_group::clarinet_trio();
performance_group::instrument::clarinet();
}
```
<span class="caption">示例 7-21: 通过 `pub use` 使名称可引入任何代码的作用域中</span>
通过 `pub use`,现在 `main` 函数可以通过新路径 `performance_group::instrument::clarinet` 来调用 `clarinet` 函数。如果没有指定 `pub use``clarinet_trio` 函数可以在其作用域中调用 `instrument::clarinet``main` 则不允许使用这个新路径。
### 使用外部包
在第二章中我们编写了一个猜猜看游戏。那个项目使用了一个外部包,`rand`,来生成随机数。为了在项目中使用 `rand`,在 *Cargo.toml* 中加入了如下行:
<span class="filename">文件名: Cargo.toml</span>
```toml
[dependencies]
rand = "0.5.5"
```
*Cargo.toml* 中加入 `rand` 依赖告诉了 Cargo 要从 *https://crates.io* 下载 `rand` 和其依赖,并使其可在项目代码中使用。
接着,为了将 `rand` 定义引入项目包的作用域,加入一行 `use`,它以 `rand` 包名开头并列出了需要引入作用域的项。回忆一下第二章的 “生成一个随机数” 部分,我们曾将 `Rng` trait 引入作用域并调用了 `rand::thread_rng` 函数:
```rust,ignore
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1, 101);
}
```
*https://crates.io* 上有很多社区成员发布的包,将其引入你自己的项目涉及到相同的步骤:在 *Cargo.toml* 列出它们并通过 `use` 将其中定义的项引入项目包的作用域中。
注意标准库(`std`)对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 *Cargo.toml* 来引入 `std`,不过需要通过 `use` 将标准库中定义的项引入项目包的作用域中来引用它们,比如 `HashMap`
```rust
use std::collections::HashMap;
```
这是一个以标注库 crate 名 `std` 开头的绝对路径。
### 嵌套路径来消除大量的 `use`
当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如猜猜看章节示例 2-4 中有两行 `use` 语句都从 `std` 引入项到作用域:
<span class="filename">文件名: src/main.rs</span>
```rust
use std::cmp::Ordering;
use std::io;
// ---snip---
```
可以使用嵌套的路径将同样的项在一行中引入而不是两行,这么做需要指定路径的相同部分,接着是两个分号,接着是大括号中的各自不同的路径部分,如示例 7-22 所示。
<span class="filename">文件名: src/main.rs</span>
```rust
use std::{cmp::Ordering, io};
// ---snip---
```
<span class="caption">示例 7-22: 指定嵌套的路径在一行中将多个带有相同前缀的项引入作用域</span>
在从相同包或模块中引入很多项的程序中,使用嵌套路径显著减少所需的单独 `use` 语句!
也可以剔除掉完全包含在另一个路径中的路径。例如,示例 7-23 中展示了两个 `use` 语句:一个将 `std::io` 引入作用域,另一个将 `std::io::Write` 引入作用域:
<span class="filename">文件名: src/lib.rs</span>
```rust
use std::io;
use std::io::Write;
```
<span class="caption">示例 7-23: 通过两行 `use` 语句引入两个路径,其中一个是另一个的子路径</span>
两个路径的相同部分是 `std::io`,这正是第一个路径。为了在一行 `use` 语句中引入这两个路径,可以在嵌套路径中使用 `self`,如示例 7-24 所示。
<span class="filename">文件名: src/lib.rs</span>
```rust
use std::io::{self, Write};
```
<span class="caption">示例 7-24: 将示例 7-23 中部分重复的路径合并为一个 `use` 语句</span>
这将 `std::io``std::io::Write` 同时引入作用域。
### 通过 glob 运算符将所有的公有定义引入作用域
如果希望将一个路径下 **所有** 公有项引入作用域,可以指定路径后跟 `*`glob 运算符:
```rust
use std::collections::*;
```
这个 `use` 语句将 `std::collections` 中定义的所有公有项引入当前作用域。
使用 glob 运算符时请多加小心!如此难以推导作用域中有什么名称和它们是在何处定义的。
glob 运算符经常用于测试模块 `tests` 中,这时会将所有内容引入作用域;我们将在第十一章 “如何编写测试” 部分讲解。glob 运算符有时也用于 prelude 模式;查看 [标准库中的文档](https://doc.rust-lang.org/std/prelude/index.html#other-preludes) 了解这个模式的更多细节。
### 将模块分割进不同文件
目前本章所有的例子都在一个文件中定义多个模块。当模块变得更大时,你可能想要将它们的定义移动到一个单独的文件中使代码更容易阅读。
例如从示例 7-8 的代码开始,我们可以通过修改 crate 根文件(这里是 *src/main.rs*)将 `sound` 模块移动到其自己的文件 *src/sound.rs* 中,如示例 7-25 所示。
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
mod sound;
fn main() {
// 绝对路径
crate::sound::instrument::clarinet();
// 相对路径
sound::instrument::clarinet();
}
```
<span class="caption">示例 7-25: 声明 `sound` 模块,其内容位于 *src/sound.rs* 文件</span>
*src/sound.rs* 中会包含 `sound` 模块的内容,如示例 7-26 所示。
<span class="filename">文件名: src/sound.rs</span>
```rust
pub mod instrument {
pub fn clarinet() {
// 函数体
}
}
```
<span class="caption">示例 7-26: `sound` 模块中的定义位于 *src/sound.rs*</span>
`mod sound` 后使用分号而不是代码块告诉 Rust 在另一个与模块同名的文件中加载模块的内容。
继续重构我们例子,将 `instrument` 模块也提取到其自己的文件中,修改 *src/sound.rs* 只包含 `instrument` 模块的声明:
<span class="filename">文件名: src/sound.rs</span>
```rust
pub mod instrument;
```
接着创建 *src/sound* 目录和 *src/sound/instrument.rs* 文件来包含 `instrument` 模块的定义:
<span class="filename">文件名: src/sound/instrument.rs</span>
```rust
pub fn clarinet() {
// 函数体
}
```
模块树依然保持相同,`main` 中的函数调用也无需修改继续保持有效,即使其定义存在于不同的文件中。这样随着代码增长可以将模块移动到新文件中。
## 总结
Rust 提供了将包组织进 crate、将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。可以通过 `use` 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认时私有的,不过可以选择增加 `pub` 关键字使其定义变为公有。
接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。

@ -1,8 +1,8 @@
# 通用集合类型
> [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-00-common-collections.md)
> [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/master/src/ch08-00-common-collections.md)
> <br>
> commit 54e81980185fbb1a4cb5a18dce1dc6deeb66b573
> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca
Rust 标准库中包含一系列被称为 **集合***collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终成长的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:
@ -14,4 +14,4 @@ Rust 标准库中包含一系列被称为 **集合***collections*)的非常
[collections]: https://doc.rust-lang.org/std/collections
我们将讨论如何创建和更新 vector、字符串和哈希 map以及它们有什么不同
我们将讨论如何创建和更新 vector、字符串和哈希 map以及它们有什么特别之处

@ -1,8 +1,8 @@
## vector 用来储存一系列的值
> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-01-vectors.md)
> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/src/ch08-01-vectors.md)
> <br>
> commit 550c8ea6f74060ff1f7b67e7e1878c4da121682d
> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37
我们要讲到的第一个类型是 `Vec<T>`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个的值它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
@ -16,7 +16,7 @@ let v: Vec<i32> = Vec::new();
<span class="caption">示例 8-1新建一个空的 vector 来储存 `i32` 类型的值</span>
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。在示例 8-1 中,我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。
在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个 `Vec`,而且为了方便 Rust 提供了 `vec!` 宏。这个宏会根据我们提供的值来创建一个新的 `Vec`。示例 8-2 新建一个拥有值 `1`、`2` 和 `3``Vec<i32>`
@ -53,9 +53,9 @@ v.push(8);
{
let v = vec![1, 2, 3, 4];
// do stuff with v
// 处理变量 v
} // <- v goes out of scope and is freed here
} // <- v
```
<span class="caption">示例 8-4展示 vector 和其元素于何处被丢弃</span>
@ -72,7 +72,12 @@ v.push(8);
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);
println!("The third element is {}", third);
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
```
<span class="caption">列表 8-5使用索引语法或 `get` 方法来访问 vector 中的项</span>
@ -81,7 +86,7 @@ let third: Option<&i32> = v.get(2);
Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。作为一个例子,让我们看看如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素时程序会如何处理,如示例 8-6 所示:
```rust,should_panic
```rust,should_panic,panics
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
@ -90,20 +95,20 @@ let does_not_exist = v.get(100);
<span class="caption">示例 8-6尝试访问一个包含 5 个元素的 vector 的索引 100 处的元素</span>
当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 `panic!`。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。
当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 panic。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。
`get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)``None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!
#### 无效引用
一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第四章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的:
一旦程序获取了一个有效的引用,借用检查器将会执行第四章讲到的所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的:
```rust,ignore
```rust,ignore,does_not_compile
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
```
<span class="caption">示例 8-7在拥有 vector 中项的引用的同时向其增加一个元素</span>
@ -112,19 +117,19 @@ v.push(6);
```text
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
-->
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^ mutable borrow occurs here
7 |
8 | }
| - immutable borrow ends here
--> src/main.rs:10:5
|
8 | let first = &v[0];
| - immutable borrow occurs here
9 |
10 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
11 |
12 | println!("The first element is: {}", first);
| ----- borrow later used here
```
示例 8-7 中的代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
示例 8-7 中的代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
> 注意:关于 `Vec<T>` 类型的更多实现细节,在 *https://doc.rust-lang.org/stable/nomicon/vec.html* 查看 “The Nomicon”
@ -152,7 +157,7 @@ for i in &mut v {
<span class="caption">示例8-9遍历 vector 中元素的可变引用</span>
为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。
为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。第十五章会详细介绍 `*`
### 使用枚举来储存多种类型

@ -1,10 +1,10 @@
## 使用字符串存储 UTF-8 编码的文本
> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-02-strings.md)
> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/src/ch08-02-strings.md)
> <br>
> commit d0e83220e083ef87880e6a04f030b90c9af9385b
> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37
第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面内容的结合Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。
第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的程序员就可能显得很困难了。
在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引` String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。
@ -26,16 +26,14 @@ let mut s = String::new();
<span class="caption">示例 8-11新建一个空的 `String`</span>
这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。
通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,字符串字面值也实现了它。示例 8-12 展示了两个例子。
这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,字符串字面值也实现了它。示例 8-12 展示了两个例子。
```rust
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
// 该方法也可直接用于字符串字面值:
let s = "initial contents".to_string();
```
@ -71,13 +69,13 @@ let hello = String::from("Hola");
<span class="caption">示例 8-14在字符串中储存不同语言的问候语</span>
所有这些都是有效的 `String`值。
所有这些都是有效的 `String` 值。
### 更新字符串
`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,`String` 实现了 `+` 运算符作为连接运算符以便于使用
`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,可以方便的使用 `+` 运算符或 `format!` 宏来拼接 `String`
#### 使用 push 附加字符串
#### 使用 `push_str` 和 `push` 附加字符串
可以通过 `push_str` 方法来附加字符串 slice从而使 `String` 变长,如示例 8-15 所示。
@ -101,7 +99,7 @@ println!("s2 is {}", s2);
如果 `push_str` 方法获取了 `s2` 的所有权,就不能在最后一行打印出其值了。好在代码如我们期望那样工作!
`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 l 加入 `String` 的代码。
`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 *l* 加入 `String` 的代码。
```rust
let mut s = String::from("lo");
@ -112,14 +110,14 @@ s.push('l');
执行这些代码之后,`s` 将会包含 “lol”。
#### 使用 + 运算符或 `format!` 宏连接字符串
#### 使用 `+` 运算符或 `format!` 宏拼接字符串
通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用 `+` 运算符,如示例 8-18 所示。
```rust
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
```
<span class="caption">示例 8-18使用 `+` 运算符将两个 `String` 值合并到一个新的 `String` 值中</span>
@ -132,11 +130,11 @@ fn add(self, s: &str) -> String {
这并不是标准库中实际的签名;标准库中的 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那微妙部分的线索。
首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str``String` 相加,不能将两个 `String` 值相加。不过等一下——正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么示例 8-18 还能编译呢?
首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str``String` 相加,不能将两个 `String` 值相加。不过等一下 —— 正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么示例 8-18 还能编译呢?
之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转***coerced*)成 `&str`——当`add`函数被调用时Rust 使用了一个被称为 **解引用强制多态***deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第十五章会更深入的讨论解引用强制多态。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`
之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转***coerced*)成 `&str`当`add`函数被调用时Rust 使用了一个被称为 **解引用强制多态***deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第十五章会更深入的讨论解引用强制多态。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`
其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着上面例子中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着示例 8-18 中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
如果想要级联多个字符串,`+` 的行为就显得笨重了:
@ -164,7 +162,7 @@ let s = format!("{}-{}-{}", s1, s2, s3);
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 `String` 的一部分,会出现一个错误。考虑一下如示例 8-19 中所示的无效代码。
```rust,ignore
```rust,ignore,does_not_compile
let s1 = String::from("hello");
let h = s1[0];
```
@ -193,7 +191,7 @@ error[E0277]: the trait bound `std::string::String: std::ops::Index<{integer}>`
let len = String::from("Hola").len();
```
在这里,`len` 的值是 4 ,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3
在这里,`len` 的值是 4 ,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3
```rust
let len = String::from("Здравствуйте").len();
@ -201,7 +199,7 @@ let len = String::from("Здравствуйте").len();
当问及这个字符是多长的时候有人可能会说是 12。然而Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码:
```rust,ignore
```rust,ignore,does_not_compile
let hello = "Здравствуйте";
let answer = &hello[0];
```
@ -291,8 +289,6 @@ for b in "नमस्ते".bytes() {
```text
224
164
168
224
// --snip--
165
135

@ -1,14 +1,14 @@
## 哈希 map 储存键值对
> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md)
> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/src/ch08-03-hash-maps.md)
> <br>
> commit c2fd7b2d39c4130dd17bb99c101ac94af83d1a44
> commit d073ece693e880b69412e645e4eabe99e74e7590
最后介绍的常用集合类型是 **哈希 map***hash map*)。`HashMap<K, V>` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个 **哈希函数***hashing function*来实现映射决定如何将键和值放入内存中。很多编程语言支持这种数据结构不过通常有不同的名字哈希、map、对象、哈希表或者关联数组仅举几例。
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到们的得分。
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到们的得分。
本章我们会介绍哈希 map 的基本 API不过还有更多吸引人的功能隐藏于标准库中 `HashMap` 定义的函数中。请一如既往地查看标准库文档来了解更多信息。
本章我们会介绍哈希 map 的基本 API不过还有更多吸引人的功能隐藏于标准库中 `HashMap<K, V>` 定义的函数中。一如既往请查看标准库文档来了解更多信息。
### 新建一个哈希 map
@ -56,8 +56,8 @@ let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
// 这里 field_name 和 field_value 不再有效,
// 尝试使用它们看看会出现什么编译错误!
```
<span class="caption">示例 8-22展示一旦键值对被插入后就为哈希 map 所拥有</span>
@ -178,7 +178,9 @@ println!("{:?}", map);
### 哈希函数
`HashMap` 默认使用一种密码学安全的哈希函数它可以抵抗拒绝服务Denial of Service, DoS攻击。然而这并不是可用的最快的算法不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢以致于你无法接受你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hashercrates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
`HashMap` 默认使用一种 “密码学安全的”“cryptographically strong” [^siphash] 哈希函数它可以抵抗拒绝服务Denial of Service, DoS攻击。然而这并不是可用的最快的算法不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢以致于你无法接受你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
[^siphash]: [https://www.131002.net/siphash/siphash.pdf](https://www.131002.net/siphash/siphash.pdf)
## 总结

@ -1,11 +1,11 @@
# 错误处理
> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-00-error-handling.md)
> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/src/ch09-00-error-handling.md)
> <br>
> commit 7f0c806e746a133ab344cb2035d31f2a63fb6d79
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
Rust 对可靠性的执着也延伸到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多特性来处理出现错误的情况。在很多情况下Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。
Rust 对可靠性的执着也延伸到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多特性来处理出现错误的情况。在很多情况下Rust 要求你承认出错的可能性并在编译代码之前就采取行动。这些要求使得程序更为健壮,它们确保了你会在将代码部署到生产环境之前就发现错误并正确地处理它们!
Rust 将错误组合成两个主要类别:**可恢复错误***recoverable*)和 **不可恢复错误***unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
大部分语言并不区分这两类错误并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有 `Result<T, E>` 值,以及 `panic!`,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍 `panic!` 调用,接着会讲到如何返回 `Result<T, E>`。此外,我们将在决定是尝试从错误中恢复还是停止执行时探讨注意事项。
大部分语言并不区分这两类错误并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有 `Result<T, E>` 值,以及 `panic!`,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍 `panic!` 调用,接着会讲到如何返回 `Result<T, E>`。此外,我们将探讨决定是尝试从错误中恢复还是停止执行时的注意事项。

@ -1,14 +1,14 @@
## `panic!` 与不可恢复的错误
> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-01-unrecoverable-errors-with-panic.md)
> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/src/ch09-01-unrecoverable-errors-with-panic.md)
> <br>
> commit 609909ec443042399858d1f679b0df1d6d0eba22
> commit d073ece693e880b69412e645e4eabe99e74e7590
突然有一天,糟糕的事情发生而你对此束手无策。对于这种情况Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。
突然有一天,代码出问题而你对此束手无策。对于这种情况Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。
> ### Panic 中的栈展开与终止
> ### 对应 panic 时的栈展开或终止
>
> 当出现 `panic!` 时,程序默认会开始 **展开***unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 **终止***abort*这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好panic 时通过在 *Cargo.toml*`[profile]` 部分增加 `panic = 'abort'`可以由展开切换为终止。例如如果你想要在release模式中 panic 时直接终止:
> 当出现 panic 时,程序默认会开始 **展开***unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 **终止***abort*这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好panic 时通过在 *Cargo.toml*`[profile]` 部分增加 `panic = 'abort'`可以由展开切换为终止。例如如果你想要在release模式中 panic 时直接终止:
>
> ```toml
> [profile.release]
@ -19,7 +19,7 @@
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic
```rust,should_panic,panics
fn main() {
panic!("crash and burn");
}
@ -36,7 +36,7 @@ thread 'main' panicked at 'crash and burn', src/main.rs:2:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.
```
最后三行包含 `panic!` 造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:4* 表明这是 *src/main.rs* 文件的第二行第四个字符。
最后两行包含 `panic!` 调用造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:4* 表明这是 *src/main.rs* 文件的第二行第四个字符。
在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 `panic!` 宏的调用。在其他情况下,`panic!` 可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。可以使用 `panic!` 被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。下面我们会详细介绍 backtrace 是什么。
@ -46,7 +46,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic
```rust,should_panic,panics
fn main() {
let v = vec![1, 2, 3];
@ -56,7 +56,7 @@ fn main() {
<span class="caption">示例 9-1尝试访问超越 vector 结尾的元素,这会造成 `panic!`</span>
这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。
这里尝试访问 vector 的第一百个元素(这里的索引是 99 因为索引从 0 开始),不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。
这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 **缓冲区溢出***buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。
@ -70,12 +70,11 @@ $ cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
99', /checkout/src/liballoc/vec.rs:1555:10
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
```
这指向了一个不是我们编写的文件,*libcollections/vec.rs*。这是标准库中 `Vec<T>` 的实现。这是当对 vector `v` 使用 `[]`*libcollections/vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。
这指向了一个不是我们编写的文件,*vec.rs*。这是标准库中 `Vec<T>` 的实现。这是当对 vector `v` 使用 `[]`*vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。
接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace *backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们尝试获取一个 backtrace示例 9-2 展示了与你看到类似的输出:
接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace *backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们`RUST_BACKTRACE` 环境变量设置为任何不是 0 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出:
```text
$ RUST_BACKTRACE=1 cargo run
@ -121,8 +120,8 @@ stack backtrace:
<span class="caption">示例 9-2当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息</span>
这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,这里便是如此
这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace必须启用 debug 标识。当不使用 `--release` 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样
示例 9-2 的输出中backtrace 的 11 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic第一个提到我们编写的代码行的位置是你应该开始调查的以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic以及应当如何处理才能避免这个问题。
示例 9-2 的输出中backtrace 的 11 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic第一个提到我们编写的代码行的位置是你应该开始调查的以便查明是什么值如何在这个地方引起了 panic。在示例 9-1 中,我们故意编写会 panic 的代码来演示如何使用 backtrace修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic以及应当如何处理才能避免这个问题。
本章后面的小节“panic! 还是不 panic!”会再次回到 `panic!` 并讲到何时应该及何时不应该使用这个方式。接下来,我们来看看如何使用 `Result` 来从错误中恢复。
本章后面的小节 “panic! 还是不 panic!”会再次回到 `panic!` 会回到 `panic!` 并讲解何时应该何时不应该使用 `panic!` 来处理错误情况。接下来,我们来看看如何使用 `Result` 来从错误中恢复。

@ -1,8 +1,8 @@
## `Result` 与可恢复的错误
> [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-02-recoverable-errors-with-result.md)
> [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/master/src/ch09-02-recoverable-errors-with-result.md)
> <br>
> commit 347a8bf6beaf34cef5c4e82c2171f498f081485e
> commit db53e2e3cdf77beac853df6f29db4b3b86ea598c
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而失败,此时我们可能想要创建这个文件而不是终止进程。
@ -33,7 +33,7 @@ fn main() {
<span class="caption">示例 9-3打开文件</span>
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下:
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看 [标准库 API 文档](https://doc.rust-lang.org/std/index.html)<!-- ignore -->,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下:
```rust,ignore
let f: u32 = File::open("hello.txt");
@ -99,6 +99,9 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
<span class="filename">文件名: src/main.rs</span>
<!-- ignore this test because otherwise it creates hello.txt which causes other
tests to fail lol -->
```rust,ignore
use std::fs::File;
use std::io::ErrorKind;
@ -108,22 +111,12 @@ fn main() {
let f = match f {
Ok(file) => file,
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
@ -131,15 +124,36 @@ fn main() {
<span class="caption">示例 9-5使用不同的方式处理不同类型的错误</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`,它代表尝试打开的文件并不存在。所以 `match``f` 匹配,不过对于 `error.kind()` 还有一个内部 `match`
我们希望在匹配守卫中检查的条件是 `error.kind()` 的返回值是 `ErrorKind``NotFound` 成员。如果是,则尝试通过 `File::create` 创建文件。然而因为 `File::create` 也可能会失败,还需要增加一个内部 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 `match` 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。
这里有好多 `match``match` 确实很强大不过也非常的基础。第十三章我们会介绍闭包closure。`Result<T, E>` 有很多接受闭包的方法,并采用 `match` 表达式实现。一个更老练的 Rustacean 可能会这么写:
```rust,ignore
use std::fs::File;
use std::io::ErrorKind;
条件 `if error.kind() == ErrorKind::NotFound` 被称作 *match guard*:它是一个进一步完善 `match` 分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑 `match` 中的下一个分支。模式中的 `ref` 是必须的,这样 `error` 就不会被移动到 guard 条件中而仅仅只是引用它。第十八章会详细解释为什么在模式中使用 `ref` 而不是 `&` 来获取一个引用。简而言之,在模式的上下文中,`&` 匹配一个引用并返回它的值,而 `ref` 匹配一个值并返回一个引用。
fn main() {
let f = File::open("hello.txt").map_err(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Tried to create file but there was a problem: {:?}", error);
})
} else {
panic!("There was a problem opening the file: {:?}", error);
}
});
}
```
在 match guard 中我们想要检查的条件是 `error.kind()` 是否是 `ErrorKind` 枚举的 `NotFound` 成员。如果是,尝试用 `File::create` 创建文件。然而 `File::create` 也可能会失败,还需要增加一个内部 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 `match` 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。
阅读完第十三章后再回到这个例子,并查看标准库文档 `map_err``unwrap_or_else` 方法都做了什么操作。还有很多这类方法可以消除大量处理错误时嵌套的 `match` 表达式
### 失败时 panic 的简写:`unwrap` 和 `expect`
`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result<T, E>` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok``unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err``unwrap` 会为我们调用 `panic!`
`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result<T, E>` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok``unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err``unwrap` 会为我们调用 `panic!`。这里是一个实践 `unwrap` 的例子:
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic
use std::fs::File;
@ -210,7 +224,7 @@ fn read_username_from_file() -> Result<String, io::Error> {
<span class="caption">示例 9-6一个函数使用 `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-4 中的 `match`,唯一的区别是当 `Err` 时不再调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。
@ -243,7 +257,7 @@ fn read_username_from_file() -> Result<String, io::Error> {
`Result` 值之后的 `?` 被定义为与示例 9-6 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err``Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。
示例 9-6 中的 `match` 表达式与问号运算符所做的有一点不同:`?` 所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。到问号运算符调用 `from` 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 `from` 函数来定义如将其转换为返回的错误类型,问号运算符会自动处理这些转换。
示例 9-6 中的 `match` 表达式与问号运算符所做的有一点不同:`?` 所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。`?` 调用 `from` 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 `from` 函数来定义如将其转换为返回的错误类型,`?` 会自动处理这些转换。
在示例 9-7 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将一些 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`
@ -269,6 +283,23 @@ 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-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学的写法。
说到编写这个函数的不同方法,甚至还有一个更短的写法:
<span class="filename">文件名: src/main.rs</span>
```rust
use std::io;
use std::fs;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
```
<span class="caption">示例 9-9: 使用 `fs::read_to_string`</span>
将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 `fs::read_to_string` 的函数,它会打开文件、新建一个 `String`、读取文件的内容,并将内容放入 `String`,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。
### `?` 只能被用于返回 `Result` 的函数
`?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与示例 9-6 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。
@ -286,20 +317,31 @@ fn main() {
当编译这些代码,会得到如下错误信息:
```text
error[E0277]: the trait bound `(): std::ops::Try` is not satisfied
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
--> src/main.rs:4:13
|
4 | let f = File::open("hello.txt")?;
| ------------------------
| |
| the `?` operator can only be used in a function that returns
`Result` (or another type that implements `std::ops::Try`)
| in this macro invocation
| ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `std::ops::Try` is not implemented for `()`
= note: required by `std::ops::Try::from_error`
```
错误指出只能在返回 `Result` 的函数中使用问号运算符。在不返回 `Result` 的函数中,当调用其他返回 `Result` 的函数时,需要使用 `match``Result` 的方法之一来处理,而不能用 `?` 将潜在的错误传播给调用者。
错误指出只能在返回 `Result` 的函数中使用 `?`。在不返回 `Result` 的函数中,当调用其他返回 `Result` 的函数时,需要使用 `match``Result` 的方法之一来处理,而不能用 `?` 将潜在的错误传播给代码调用方。
不过 `main` 函数可以返回一个 `Result<T, E>`
```rust,ignore
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
```
`Box<dyn Error>` 被称为 “trait 对象”“trait object”第十七章会介绍。目前可以理解 `Box<dyn Error>` 为 “任何类型的错误”。
现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到他们各自适合哪些场景的话题了。

@ -1,12 +1,12 @@
## `panic!` 还是不 `panic!`
> [ch09-03-to-panic-or-not-to-panic.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-03-to-panic-or-not-to-panic.md)
> [ch09-03-to-panic-or-not-to-panic.md](https://github.com/rust-lang/book/blob/master/src/ch09-03-to-panic-or-not-to-panic.md)
> <br>
> commit 609909ec443042399858d1f679b0df1d6d0eba22
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。
有一些情况 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
有一些情况 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的。章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
### 示例、代码原型和测试都非常适合 panic
@ -38,14 +38,12 @@ let home: IpAddr = "127.0.0.1".parse().unwrap();
如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 `panic!` 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,`panic!` 通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。
无论代码编写的多么好,当有害状态是预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约***contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug而且这也不是那种你希望调用方必须处理的错误。事实上也没有合理的方式来恢复调用方的代码调用方的 **程序员** 需要修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于 `Option` 的类型,而且程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some``None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。
### 创建自定义类型作为验证
让我们使用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型以进行验证。回忆一下第二章的猜猜看游戏,我们的代码要求用户猜测一个 1 到 100 之间的数字在将其与秘密数字做比较之前我们从未验证用户的猜测是位于这两个数字之间的我们只验证它是否为正。在这种情况下其影响并不是很严重“Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。
一种实现方式是将猜测解析成 `i32` 而不仅仅是 `u32`,来默许输入负数,接着检查数字是否在范围内:
@ -73,15 +71,15 @@ loop {
然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-9 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-10 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
```rust
pub struct Guess {
value: u32,
value: i32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
@ -91,13 +89,13 @@ impl Guess {
}
}
pub fn value(&self) -> u32 {
pub fn value(&self) -> i32 {
self.value
}
}
```
<span class="caption">示例 9-9:一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span>
<span class="caption">示例 9-10:一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span>
首先,我们定义了一个包含 `u32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。
@ -109,6 +107,6 @@ impl Guess {
## 总结
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!``Result` 将会使你的代码在面对无处不在的错误时显得更加可靠。
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!``Result` 将会使你的代码在面对不可避免的错误时显得更加可靠。
现在我们已经见识过了标准库中 `Option``Result` 泛型枚举的能力了,在下一章让我们聊聊泛型是如何工作的,以及如何在你的代码中使用他们。

@ -1,10 +1,10 @@
# 泛型、trait 和生命周期
> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-00-generics.md)
> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/src/ch10-00-generics.md)
> <br>
> commit 9e8d1ed7a1732d9cc09606a9b62ee8838998502f
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
每一个编程语言都有高效的处理重复概念的工具在 Rust 中其工具之一就是 **泛型***generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
每一个编程语言都有高效的处理重复概念的工具.在 Rust 中其工具之一就是 **泛型***generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32``String` 这样的具体值。我们已经使用过第六章的 `Option<T>`,第八章的 `Vec<T>``HashMap<K, V>`,以及第九章的 `Result<T, E>` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法!
@ -45,7 +45,7 @@ fn main() {
如果需要在两个不同的列表中寻找最大值,我们可以重复示例 10-1 中的代码,这样程序中就会存在两段相同逻辑的代码,如示例 10-2 所示:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
fn main() {
@ -79,10 +79,6 @@ fn main() {
虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
<!-- Are we safe assuming the reader will be familiar with the term
"abstraction" in this context, or do we want to give a brief definition? -->
<!-- Yes, our audience will be familiar with this term. /Carol -->
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。
在示例 10-3 的程序中将寻找最大值的代码提取到了一个叫做 `largest` 的函数中。这个程序可以找出两个不同数字列表的最大值,不过示例 10-1 中的代码只存在于一个位置:
@ -123,9 +119,9 @@ fn main() {
从示例 10-2 到示例 10-3 中涉及的机制经历了如下几步:
1. 我们注意到了重复代码。
2. 我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
3. 我们将重复代码的两个实例,改为调用函数。
1. 找出重复代码。
2. 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
3. 将重复代码的两个实例,改为调用函数。
在不同的场景使用不同的方式,我们也可以利用相同的步骤和泛型来减少重复代码。与函数体可以在抽象`list`而不是特定值上操作的方式相同,泛型允许代码对抽象类型进行操作。

@ -1,17 +1,16 @@
## 泛型数据类型
> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-01-syntax.md)
> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-01-syntax.md)
> <br>
> commit 9e8d1ed7a1732d9cc09606a9b62ee8838998502f
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
泛型用于为函数签名或结构体等创建定义,允许我们创建许多不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。
我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。
### 在函数定义中使用泛型
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到 `largest` 函数上,示例 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从示例 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。第二个函数寻找 slice 中 `char` 的最大值:
当使用泛型定义函数时,我们在函数签名中通常为参数和返回值指定数据类型的位置放置泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到 `largest` 函数上,示例 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。
<span class="filename">文件名: src/main.rs</span>
@ -57,13 +56,11 @@ fn main() {
<span class="caption">示例 10-4两个只在名称和签名中类型有所不同的函数</span>
这里 `largest_i32``largest_char` 有着完全相同的函数体,所以如果能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称 `T`。任何标识符都可以作为类型参数名,选择 `T` 是因为 Rust 的类型命名规范是骆驼命名法CamelCase。另外泛型类型参数的规范也倾向于简短经常仅仅是一个字母。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。
`largest_i32` 函数是从示例 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。`largest_char` 函数寻找 slice 中 `char` 的最大值:这两个函数有着相同的代码,所以让我们在一个单独的函数中引入泛型参数来消除重复。
当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。任何标识符都可以作为类型参数名。不过选择 `T` 是因为 Rust 的习惯是让变量名尽量短,通常就只有一个字母,同时 Rust 类型命名规范是骆驼命名法CamelCase。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。
我们将要定义的泛型版本的 `largest` 函数的签名看起来像这样:
当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的 `largest` 函数,类型参数声明位于函数名称与参数列表中间的尖括号 `<>` 中,像这样:
```rust,ignore
fn largest<T>(list: &[T]) -> T {
@ -71,11 +68,11 @@ fn largest<T>(list: &[T]) -> 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>
```rust,ignore
```rust,ignore,does_not_compile
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
@ -107,22 +104,17 @@ fn main() {
```text
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^
| ^^^^^^^^^^^^^^
|
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
```
注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方
注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能(查看附录 C 获取该 trait 的更多信息)
<!-- Liz: this is the reason we had the topics in the order we did in the first
draft of this chapter; it's hard to do anything interesting with generic types
in functions unless you also know about traits and trait bounds. I think this
ordering could work out okay, though, and keep a stronger thread with the
`longest` function going through the whole chapter, but we do pause with a
not-yet-compiling example here, which I know isn't ideal either. Let us know
what you think. /Carol -->
标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能。在 “trait bound” 部分会讲解如何指定泛型实现特定的 trait不过让我们先探索其他使用泛型参数的方法。
### 结构体定义中的泛型
@ -146,11 +138,11 @@ fn main() {
其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意 `Point` 的定义中只使用了一个泛型类型,我们想要表达的是结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x``y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像示例 10-7 中的代码就不能编译:
注意 `Point` 的定义中只使用了一个泛型类型,这个定义表明结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x``y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像示例 10-7 中的代码就不能编译:
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
struct Point<T> {
x: T,
y: T,
@ -163,22 +155,20 @@ fn main() {
<span class="caption">示例 10-7字段 `x``y` 必须是相同类型,因为他们都有相同的泛型类型 `T`</span>
尝试编译会得到如下错误:
在这个例子中,当把整型值 5 赋值给 `x` 时,就告诉了编译器这个 `Point<T>` 实例中的泛型 `T` 是整型的。接着指定 `y` 为 4.0,它被定义为与 `x` 相同类型,就会得到一个像这样的类型不匹配错误:
```text
error[E0308]: mismatched types
-->
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integral variable, found
floating-point variable
floating-point variable
|
= note: expected type `{integer}`
= note: found type `{float}`
found type `{float}`
```
当我们将 5 赋值给 `x`,编译器就知道这个 `Point` 实例的泛型类型 `T` 是一个整型。接着我们将 `y` 指定为 4.0,而它被定义为与 `x` 有着相同的类型,所以出现了类型不匹配的错误。
如果想要定义一个 `x``y` 可以有不同类型且仍然是泛型的 `Point` 结构体,我们可以使用多个泛型类型参数。在示例 10-8 中,我们修改 `Point` 的定义为拥有两个泛型类型 `T``U`。其中字段 `x``T` 类型的,而字段 `y``U` 类型的:
<span class="filename">文件名: src/main.rs</span>
@ -200,9 +190,9 @@ fn main() {
现在所有这些 `Point` 实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。当你的代码中需要许多泛型类型时,它可能表明你的代码需要重组为更小的部分。
### 枚举定义中的泛型数据类型
### 枚举定义中的泛型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 `Option<T>` 枚举,现在这个定义看起来就更容易理解了。让我们再看看:
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 `Option<T>` 枚举,让我们再看看:
```rust
enum Option<T> {
@ -211,7 +201,7 @@ enum Option<T> {
}
```
换句话说 `Option<T>` 是一个拥有泛型 `T` 的枚举。它有两个成员:`Some`,它存放了一个类型 `T` 的值,和不存在任何值的`None`。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值” 是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念
现在这个定义看起来就更容易理解了。如你所见 `Option<T>` 是一个拥有泛型 `T` 的枚举,它有两个成员:`Some`,它存放了一个类型 `T` 的值,和不存在任何值的`None`。通过 `Option<T>` 枚举可以表达有一个可能的值的抽象概念,同时因为 `Option<T>` 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象
枚举也可以拥有多个泛型类型。第九章使用过的 `Result` 枚举定义就是一个这样的例子:
@ -226,9 +216,11 @@ enum Result<T, E> {
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。
### 方法定义中的枚举数据类型
### 方法定义中的泛型
也可以在定义中使用泛型在结构体和枚举上实现方法(像第五章那样)。
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point<T>`。接着我们在 `Point<T>` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用:
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point<T>`,和在其上实现的名为 `x` 的方法。
<span class="filename">文件名: src/main.rs</span>
@ -253,7 +245,11 @@ fn main() {
<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`
这里在 `Point<T>` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用:
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。
例如,可以选择为 `Point<f32>` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`
```rust
# struct Point<T> {
@ -270,7 +266,7 @@ impl Point<f32> {
<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>` 实例则没有定义此方法。这个方法计算点实例与坐标 (0.0, 0.0) 之间的距离,并使用了只能用于浮点型的数学运算符。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中在示例 10-8 中的结构体 `Point<T, U>` 上定义了一个方法 `mixup`。这个方法获取另一个 `Point` 作为参数,而它可能与调用 `mixup``self` 是不同的 `Point` 类型。这个方法用 `self``Point` 类型的 `x` 值(类型 `T`)和参数的 `Point` 类型的 `y` 值(类型 `W`)来创建一个新 `Point` 类型的实例:
@ -305,11 +301,11 @@ fn main() {
`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`
注意泛型参数 `T``U` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `V``W` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。
这个例子的目的是展示一些泛型通过 `impl` 声明而另一些通过方法定义声明的情况。这里泛型参数 `T``U` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `V``W` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。
### 泛型代码的性能
在阅读本部分内容的同时你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失
在阅读本部分内容的同时你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失
Rust 通过在编译时进行泛型代码的 **单态化***monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

Loading…
Cancel
Save