diff --git a/src/ch08-00-common-collections.md b/src/ch08-00-common-collections.md index e5fef765..5e249cc1 100644 --- a/src/ch08-00-common-collections.md +++ b/src/ch08-00-common-collections.md @@ -1,16 +1,15 @@ # 常见集合 - - +[ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/2581c23b669eff30c26e036a13475ec5cf70c1b8/src/ch08-00-common-collections.md) -Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。每种集合都有着不同功能和开销,而根据当前情况选择合适的集合,这是一项需要逐渐掌握的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合: +Rust 标准库中包含一些非常有用的数据结构,它们被称为 **集合**(*collections*)。大多数其他数据类型表示的是一个特定的值,而集合可以包含多个值。与内建的数组和元组类型不同,这些集合指向的数据存储在堆上,这意味着数据量不需要在编译时已知,并且可以随着程序运行而增长或缩小。每一种集合都有不同的能力和开销,而根据当前场景选择合适的集合,是一项你会随着时间逐渐掌握的技能。本章将讨论 Rust 程序中非常常用的三种集合: -- **向量**(*vector*)允许我们一个挨着一个地储存一系列数量可变的值。 -- **字符串**(*string*)是字符的集合。我们之前见过 `String` 类型,不过在本章我们将深入了解。 -- **哈希 map**(*hash map*)允许我们将值与一个特定的键(key)相关联。这是一个叫做 *map* 的更通用的数据结构的特定实现。 +- **向量**(*vector*)允许你把数量可变的值一个挨一个地存放起来。 +- **字符串**(*string*)是字符的集合。此前我们已经提到过 `String` 类型,不过本章会更深入地讨论它。 +- **哈希映射**(*hash map*)允许你把某个值与特定的键关联起来。它是更通用的数据结构 *map* 的一种具体实现。 -对于标准库提供的其他类型的集合,请查看[文档][collections]。 +要了解标准库提供的其他集合类型,请参阅[文档][collections]。 -我们将讨论如何创建和更新 vector、字符串和哈希 map,以及它们有什么特别之处。 +我们将讨论如何创建和更新 vector、字符串和哈希映射,以及它们各自的特别之处。 [collections]: https://doc.rust-lang.org/std/collections/index.html diff --git a/src/ch08-01-vectors.md b/src/ch08-01-vectors.md index defce991..7c6a054f 100644 --- a/src/ch08-01-vectors.md +++ b/src/ch08-01-vectors.md @@ -1,13 +1,12 @@ ## 使用 Vector 储存列表 - - +[ch08-01-vectors.md](https://github.com/rust-lang/book/blob/2581c23b669eff30c26e036a13475ec5cf70c1b8/src/ch08-01-vectors.md) -我们要讲到的第一个类型是 `Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 +我们要讨论的第一种集合类型是 `Vec`,也被称为 *vector*。vector 允许你在单个数据结构中存放多个值,并把这些值在内存中彼此相邻地排列起来。vector 只能存储相同类型的值。当你有一组项目要处理时,它就很有用,例如文件中的文本行,或者购物车中商品的价格。 ### 新建 vector -为了新建一个空的 vector,可以调用 `Vec::new` 函数,如示例 8-1 所示。 +要创建一个新的空 vector,可以调用 `Vec::new` 函数,如示例 8-1 所示。 ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-01/src/main.rs:here}} @@ -15,9 +14,9 @@ 示例 8-1:新建一个空的 vector 来储存 `i32` 类型的值 -注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。在示例 8-1 中,我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。 +注意,这里我们加了一个类型注解。因为还没有往这个 vector 里插入任何值,Rust 并不知道我们打算存储什么类型的元素。这一点很重要。vector 是使用泛型实现的;第十章会讲到如何在你自己的类型上使用泛型。现在你只需要知道,标准库提供的 `Vec` 类型可以容纳任意类型。当我们创建一个用来存放特定类型的 vector 时,可以在尖括号中指定这个类型。在示例 8-1 中,我们告诉 Rust,`v` 中的 `Vec` 将存放 `i32` 类型的元素。 -通常,我们会用初始值来创建一个 `Vec` 而 Rust 会推断出储存值的类型,所以很少会需要这些类型注解。为了方便 Rust 提供了 `vec!` 宏,这个宏会根据我们提供的值来创建一个新的 vector。示例 8-2 新建一个拥有值 `1`、`2` 和 `3` 的 `Vec`。推断为 `i32` 是因为这是默认整型类型,第三章的 [“数据类型”][data-types] 讨论过: +更常见的情况是,我们会用初始值创建 `Vec`,而 Rust 会推断出你想存储的值的类型,所以很少需要写这种类型注解。Rust 还很贴心地提供了 `vec!` 宏,它会创建一个新的 vector,并把你提供的值放进去。示例 8-2 创建了一个包含 `1`、`2` 和 `3` 的新 `Vec`。这里的整数类型之所以是 `i32`,是因为它是默认整数类型,正如我们在第三章的[“数据类型”][data-types]部分讨论过的那样: ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-02/src/main.rs:here}} @@ -25,11 +24,11 @@ 示例 8-2:新建一个包含初值的 vector -因为我们提供了 `i32` 类型的初始值,Rust 可以推断出 `v` 的类型是 `Vec`,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。 +因为我们给出了 `i32` 类型的初始值,Rust 可以推断出 `v` 的类型是 `Vec`,因此这里不需要类型注解。接下来看看如何修改 vector。 ### 更新 vector -对于新建一个 vector 并向其增加元素,可以使用 `push` 方法,如示例 8-3 所示: +要先创建一个 vector 再向其中添加元素,可以使用 `push` 方法,如示例 8-3 所示: ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-03/src/main.rs:here}} @@ -37,13 +36,13 @@ 示例 8-3:使用 `push` 方法向 vector 增加值 -如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用 `mut` 关键字使其可变。放入其中的所有值都是 `i32` 类型的,而且 Rust 也根据数据做出如此判断,所以不需要 `Vec` 注解。 +和任何变量一样,如果想修改它的值,就必须像第三章讲过的那样,使用 `mut` 关键字让它变成可变的。放进去的数字都是 `i32` 类型,Rust 会从数据中推断出这一点,因此也不需要写 `Vec` 注解。 ### 读取 vector 的元素 -有两种方法引用 vector 中储存的值:通过索引或使用 `get` 方法。在接下来的示例中,为了更加清楚的说明,我们已经标注了这些函数返回的值的类型。 +有两种方式可以引用 vector 中存储的值:通过索引,或者使用 `get` 方法。在接下来的示例中,为了更清楚地说明这一点,我们给这些函数返回的值标注了类型。 -示例 8-4 展示了访问 vector 中一个值的两种方式,索引语法或者 `get` 方法: +示例 8-4 展示了访问 vector 中某个值的两种方式:索引语法和 `get` 方法。 ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-04/src/main.rs:here}} @@ -51,9 +50,9 @@ 示例 8-4:使用索引语法或 `get` 方法来访问 vector 中的项 -这里有几个细节需要注意。我们使用索引值 `2` 来获取第三个元素,因为索引是从数字 0 开始的。使用 `&` 和 `[]` 会得到一个索引位置元素的引用。当使用索引作为参数调用 `get` 方法时,会得到一个可以用于 `match` 的 `Option<&T>`。 +这里有几个细节需要注意。我们用索引值 `2` 获取第三个元素,因为 vector 的索引是从 0 开始的。使用 `&` 和 `[]` 会得到索引位置处元素的引用。当我们把索引作为参数传给 `get` 方法时,会得到一个可以与 `match` 一起使用的 `Option<&T>`。 -Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素范围之外的索引值时可以选择让程序如何运行。举个例子,让我们看看使用这个技术,尝试在当有一个 5 个元素的 vector 接着访问索引 100 位置的元素会发生什么,如示例 8-5 所示: +Rust 提供这两种引用元素的方式,是为了让你可以选择:当尝试使用超出已有元素范围的索引值时,程序该如何表现。举个例子,假设我们有一个包含 5 个元素的 vector,然后尝试分别用这两种技术访问索引 100 处的元素,看看会发生什么,如示例 8-5 所示: ```rust,should_panic,panics {{#rustdoc_include ../listings/ch08-common-collections/listing-08-05/src/main.rs:here}} @@ -61,11 +60,11 @@ Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素 示例 8-5:尝试访问一个包含 5 个元素的 vector 的索引 100 处的元素 -当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 panic。此方法适用于当你希望在尝试访问 vector 末尾之外的元素时让程序直接崩溃的场景。 +运行这段代码时,第一种 `[]` 方法会让程序 panic,因为它引用了一个不存在的元素。当你希望程序在有人尝试访问 vector 末尾之外的元素时直接崩溃,这种方式就很合适。 -当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多! +当传给 `get` 方法的索引超出了 vector 的范围时,它不会 panic,而是返回 `None`。如果在正常情况下,访问超出 vector 范围的元素偶尔是可能发生的,那么你就会使用这种方法。此时你的代码可以像第六章讨论过的那样,处理 `Some(&element)` 和 `None` 两种情况。例如,索引可能来自用户输入的数字。如果用户不小心输入了一个过大的数字,程序就会得到 `None`,这时你可以告诉用户当前 vector 中有多少项,并给他们一次重新输入有效值的机会。这就比因为一个输入错误而让程序崩溃更友好。 -一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第四章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-6,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面再次引用这个元素是行不通的: +当程序拿到了一个有效引用后,借用检查器会应用所有权和借用规则(第四章讲过),来确保这个对 vector 内容的引用以及其他任何引用都保持有效。回忆一下那条规则:在同一作用域中,不能同时拥有可变引用和不可变引用。这条规则就适用于示例 8-6:我们持有了 vector 第一个元素的不可变引用,然后又尝试在 vector 末尾添加一个元素。如果还想在函数后面继续使用那个元素,这个程序就无法通过编译: ```rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch08-common-collections/listing-08-06/src/main.rs:here}} @@ -79,13 +78,13 @@ Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素 {{#include ../listings/ch08-common-collections/listing-08-06/output.txt}} ``` -示例 8-6 中的代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。 +示例 8-6 中的代码看起来似乎应该能工作:为什么对第一个元素的引用,会在乎 vector 末尾发生的变化呢?这是由 vector 的工作方式决定的。因为 vector 会把值彼此相邻地存放在内存中,所以如果末尾追加一个新元素,而当前存放位置又没有足够空间容纳所有元素,程序就可能需要分配一块新内存,并把旧元素复制到新空间里去。在这种情况下,原来指向第一个元素的引用就会指向已释放的内存。借用规则正是为了防止程序陷入这种情况。 -> 注意:关于 `Vec` 类型的更多实现细节,请查看 [“The Rustonomicon”][nomicon] +> 注意:如果想了解 `Vec` 类型更多的实现细节,请参阅 [“The Rustonomicon”][nomicon]。 ### 遍历 vector 中的元素 -如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。示例 8-7 展示了如何使用 `for` 循环来获取 `i32` 值的 vector 中的每一个元素的不可变引用并将其打印: +如果想依次访问 vector 中的每个元素,我们会遍历所有元素,而不是一次只通过索引访问一个。示例 8-7 展示了如何使用 `for` 循环,获取一个装有 `i32` 值的 vector 中每个元素的不可变引用,并把它们打印出来: ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-07/src/main.rs:here}} @@ -93,7 +92,7 @@ Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素 示例 8-7:通过 `for` 循环遍历 vector 的元素并打印 -我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变它们。示例 8-8 中的 `for` 循环会给每一个元素加 `50`: +我们也可以遍历可变 vector 中每个元素的可变引用,从而修改所有元素。示例 8-8 中的 `for` 循环会给每个元素都加上 `50`: ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-08/src/main.rs:here}} @@ -101,15 +100,15 @@ Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素 示例 8-8:遍历 vector 中元素的可变引用 -为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。第十五章的[“通过解引用运算符追踪指针的值”][deref]部分会详细介绍解引用运算符。 +要修改可变引用所指向的值,在使用 `+=` 运算符前,必须先使用解引用运算符 `*` 取到 `i` 指向的值。第十五章的[“追踪引用的值”][deref]部分会更详细地讨论解引用运算符。 -由于借用检查器的规则,无论可变还是不可变地遍历一个 vector 都是安全的。如果尝试在示例 8-7 和 示例 8-8 的 `for` 循环体内插入或删除项,都会得到一个类似示例 8-6 代码中类似的编译错误。`for` 循环中获取的 vector 引用阻止了同时对整个 vector 进行修改。 +由于借用检查器的规则,不管是可变还是不可变地遍历 vector,都是安全的。如果我们尝试在示例 8-7 和示例 8-8 的 `for` 循环体内插入或删除项,就会得到一个和示例 8-6 类似的编译错误。`for` 循环持有的那个对 vector 的引用,会阻止对整个 vector 的同时修改。 ### 使用枚举来储存多种类型 -vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举! +vector 只能存储相同类型的值。这可能会带来不便;确实有些场景需要存放一组不同类型的值。幸运的是,枚举的各个变体都定义在同一个枚举类型之下,所以当我们需要用一个类型来表示不同种类的元素时,就可以定义并使用枚举! -例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型:那个枚举的类型。接着可以创建一个储存该枚举值的 vector,这样最终就能够储存不同类型的值了。示例 8-9 展示了这个用法: +例如,假设我们想从电子表格的一行中读取值,而这一行中有些列包含整数,有些包含浮点数,还有些是字符串。我们可以定义一个枚举,让它的各个变体分别持有这些不同类型的值,而所有这些枚举变体都会被视为同一种类型,也就是该枚举本身的类型。然后,我们就可以创建一个存放这种枚举的 vector,从而最终在其中保存不同类型的值。示例 8-9 展示了这种做法: ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-09/src/main.rs:here}} @@ -117,15 +116,15 @@ vector 只能储存相同类型的值。这是很不方便的;绝对会有需 示例 8-9:定义一个枚举,以便能在 vector 中存放不同类型的数据 -Rust 在编译时必须确切知道 vector 中的类型,这样它才能确定在堆上需要为每个元素分配多少内存。我们还必须明确这个 vector 中允许的类型。如果 Rust 允许 vector 存储任意类型,那么可能会因为一个或多个类型在对 vector 元素执行操作时导致(类型相关)错误。使用枚举加上 `match` 表达式意味着 Rust 会在编译时确保每种可能的情况都得到处理,正如第六章讲到的那样。 +Rust 必须在编译时知道 vector 中会有哪些类型,这样它才能准确知道在堆上存储每个元素需要多少内存。我们还必须明确指出这个 vector 允许哪些类型。如果 Rust 允许 vector 存放任意类型,那么在对 vector 元素执行操作时,就有可能因为某一种或多种类型而导致错误。使用枚举再配合 `match` 表达式,意味着 Rust 会像第六章所说的那样,在编译时确保每一种可能的情况都得到了处理。 -如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十八章会讲到它。 +如果在编写程序时,你并不知道运行时究竟会有哪些类型需要存进 vector,那么这种枚举技巧就不适用了。相反,你可以使用 trait 对象,第 18 章会讲到它。 -现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中 `Vec` 定义的很多其他实用方法的 [API 文档][vec-api]。例如,除了 `push` 之外还有一个 `pop` 方法,它会移除并返回 vector 的最后一个元素。 +现在我们已经讨论了一些最常见的 vector 用法,记得去看看标准库为 `Vec` 定义的许多其他实用方法的 [API 文档][vec-api]。例如,除了 `push` 之外,还有一个 `pop` 方法会移除并返回 vector 的最后一个元素。 ### 丢弃 vector 时也会丢弃其所有元素 -类似于任何其他的 `struct`,vector 在其离开作用域时会被释放,如示例 8-10 所标注的: +和其他任何 `struct` 一样,vector 会在离开作用域时被释放,如示例 8-10 所标示的那样: ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-10/src/main.rs:here}} @@ -133,11 +132,11 @@ Rust 在编译时必须确切知道 vector 中的类型,这样它才能确定 示例 8-10:展示 vector 和其元素于何处被丢弃 -当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。借用检查器确保了任何 vector 中内容的引用仅在 vector 本身有效时才可用。 +当 vector 被丢弃时,它包含的所有内容也都会被一并丢弃,这意味着它持有的整数会被清理掉。借用检查器会确保,对 vector 内容的任何引用都只会在 vector 本身有效时被使用。 让我们继续下一个集合类型:`String`! [data-types]: ch03-02-data-types.html#数据类型 [nomicon]: https://doc.rust-lang.org/nomicon/vec/vec.html [vec-api]: https://doc.rust-lang.org/std/vec/struct.Vec.html -[deref]: ch15-02-deref.html#追踪指针的值 +[deref]: ch15-02-deref.html#追踪引用的值 diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index c06d2bd0..9850437f 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -1,13 +1,12 @@ ## 使用字符串储存 UTF-8 编码的文本 - - +[ch08-02-strings.md](https://github.com/rust-lang/book/blob/2581c23b669eff30c26e036a13475ec5cf70c1b8/src/ch08-02-strings.md) 第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的程序员就可能显得很困难了。 在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在本小节中,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引 `String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。 -### 什么是字符串? +### 定义字符串 我们先定义一下**字符串**这一术语的具体意义。Rust 的核心语言中只有一种字符串类型,字符串 slice `str`,它通常以被借用的形式出现,`&str`。第四章讲到了**字符串 slices**:它们是一些对储存在别处的 UTF-8 编码字符串数据的引用。举例来说,由于字符串字面值被储存在程序的二进制输出中,因此它们也是字符串 slices。 @@ -57,7 +56,7 @@ `String` 的大小可以增加,其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,可以方便的使用 `+` 运算符或 `format!` 宏来拼接 `String` 值。 -#### 使用 `push_str` 和 `push` 附加字符串 +#### 使用 `push_str` 和 `push` 追加字符串 可以通过 `push_str` 方法来附加字符串 slice,从而使 `String` 变长,如示例 8-15 所示。 @@ -107,7 +106,7 @@ fn add(self, s: &str) -> String { 首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的**引用**与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下 —— `&s2` 的类型是 `&String`, 而不是 `add` 第二个参数所指定的 `&str`。那么为什么示例 8-18 还能编译呢? -之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`。当`add`函数被调用时,Rust 使用了一个被称为 **Deref 强制转换**(*deref coercion*)的技术,实际上会把 `&s2` 转换为 `&s2[..]`。第十五章会更深入的讨论 Deref 强制转换。因为 `add` 没有获取参数的所有权,所以在这个操作后 `s2` 仍然是有效的 `String`。 +之所以能够在 `add` 调用中使用 `&s2`,是因为编译器可以把 `&String` 参数强制转换成 `&str`。当调用 `add` 方法时,Rust 会使用一种叫做 **deref 强制转换**(*deref coercion*)的机制,这里会把 `&s2` 转换成 `&s2[..]`。第十五章会更深入地讨论 deref 强制转换。因为 `add` 不会获取 `s` 参数的所有权,所以在这个操作之后,`s2` 仍然是一个有效的 `String`。 其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有**使用 `&`。这意味着示例 8-18 中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。 @@ -168,7 +167,7 @@ let answer = &hello[0]; 为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。 -#### 字节、标量值和字形簇!天呐! +#### 字节、标量值和字形簇 这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以查看字符串:字节、标量值和字形簇(最接近人们眼中 **字母**(*letters*)的概念)。 @@ -215,7 +214,7 @@ let s = &hello[0..4]; 在使用 range 来创建字符串 slice 时要格外小心,因为这么做可能会使你的程序崩溃。 -### 遍历字符串的方法 +### 遍历字符串 操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 `chars` 方法。对 “Зд” 调用 `chars` 方法会将其分开并返回两个 `char` 类型的值,接着就可以遍历其结果来访问每一个元素了: @@ -253,10 +252,10 @@ for b in "Зд".bytes() { 从字符串中获取如同天城文这样的字形簇是很复杂的,所以标准库并没有提供这个功能。[crates.io](https://crates.io/) 上有些提供这样功能的 crate。 -### 字符串并不简单 +### 处理字符串的复杂性 -总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡相比其他语言更多地暴露出了字符串的复杂性,不过也使你在开发周期后期免于处理涉及非 ASCII 字符的错误。 +总而言之,字符串是复杂的。不同的编程语言会选择不同的方式,把这种复杂性呈现给程序员。Rust 选择把正确处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员必须在一开始就更多地思考如何处理 UTF-8 数据。这种权衡比其他编程语言更直接地暴露了字符串的复杂性,但它能避免你在开发周期的后期再去处理那些涉及非 ASCII 字符的错误。 -好消息是标准库提供了很多围绕 `String` 和 `&str` 构建的功能,来帮助我们正确处理这些复杂场景。请务必查看这些使用方法的文档,例如 `contains` 来搜索一个字符串,和 `replace` 将字符串的一部分替换为另一个字符串。 +好消息是,标准库围绕 `String` 和 `&str` 构建了很多功能,来帮助我们正确处理这些复杂场景。请务必查看相关文档,了解一些有用的方法,例如用 `contains` 搜索字符串,或用 `replace` 把字符串的一部分替换成另一段字符串。 -现在让我们转向一些不太复杂的集合:哈希 map! +现在让我们转向一种稍微没那么复杂的集合:哈希映射! diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index 1e2d7c62..bdf632a6 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -1,7 +1,6 @@ ## 使用 Hash Map 储存键值对 - - +[ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/2581c23b669eff30c26e036a13475ec5cf70c1b8/src/ch08-03-hash-maps.md) 最后介绍的常用集合类型是**哈希 map**(*hash map*)。`HashMap` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个**哈希函数**(*hashing function*)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:**哈希**、**map**、**对象**、**哈希表**、**字典**或者**关联数组**,仅举几例。 @@ -48,7 +47,7 @@ Yellow: 50 Blue: 10 ``` -### 哈希 map 和所有权 +### 在哈希 map 中管理所有权 对于像 `i32` 这样的实现了 `Copy` trait 的类型,其值可以拷贝进哈希 map。对于像 `String` 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者,如示例 8-22 所示: @@ -60,7 +59,7 @@ Blue: 10 当 `insert` 调用将 `field_name` 和 `field_value` 移动到哈希 map 中后,将不能使用这两个绑定。 -如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章 [“生命周期确保引用有效”][validating-references-with-lifetimes] 部分将会更多的讨论这个问题。 +如果我们把对值的引用插入哈希 map,这些值本身并不会被移动进哈希 map。引用所指向的值必须至少在哈希 map 有效的那段时间里一直有效。第十章的[“生命周期确保引用有效”][validating-references-with-lifetimes]部分会更详细地讨论这个问题。 ### 更新哈希 map @@ -82,9 +81,9 @@ Blue: 10 #### 只在键尚不存在时插入键值对 -我们经常会检查某个特定的键是否已经存在于哈希 map 中并进行如下操作:如果哈希 map 中键已经存在则不做任何操作;如果不存在则连同值一块插入。 +我们经常会检查某个特定的键是否已经在哈希 map 中有对应的值,然后执行如下操作:如果这个键已经存在,就让原来的值保持不变;如果这个键不存在,就插入它和它对应的值。 -为此哈希 map 有一个专用的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举 `Entry` 它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 `50`,对于蓝队也是如此。使用 `entry` API 的代码看起来如示例 8-24 所示。 +Hash map 为这种场景提供了一个特殊的 API,叫做 `entry`,它接收你想检查的键作为参数。`entry` 方法的返回值是一个名为 `Entry` 的枚举,它表示一个可能存在、也可能不存在的值。假设我们想检查黄队这个键是否已经有关联的值。如果没有,就插入值 `50`;蓝队也是同样的处理方式。使用 `entry` API 的代码如示例 8-24 所示。 ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-24/src/main.rs:here}} @@ -92,7 +91,7 @@ Blue: 10 示例 8-24:使用 `entry` 方法只在键没有对应一个值时插入 -`Entry` 的 `or_insert` 方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。 +`Entry` 上的 `or_insert` 方法被定义为:如果对应 `Entry` 的键已经存在,就返回该值的可变引用;如果不存在,就把参数作为这个键的新值插入,并返回这个新值的可变引用。这比我们自己手写逻辑要清晰得多,而且和借用检查器的配合也更好。 运行示例 8-24 的代码会打印出 `{"Yellow": 50, "Blue": 10}`。第一个 `entry` 调用会插入黄队的键和值 `50`,因为黄队并没有一个值。第二个 `entry` 调用不会改变哈希 map 因为蓝队已经有了值 `10`。 @@ -112,7 +111,7 @@ Blue: 10 ### 哈希函数 -`HashMap` 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)[^siphash] 的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。[第十章][traits]会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 +`HashMap` 默认使用一种叫做 SipHash 的哈希函数,它可以提供对涉及哈希表[^siphash]的拒绝服务(Denial of Service, DoS)攻击的抵抗能力。不过这不是目前可用的最快哈希算法,但为了更好的安全性而接受一些性能下降,是值得的权衡。如果你分析代码后发现默认哈希函数对你的用途来说太慢,就可以通过指定不同的 hasher 来切换到其他函数。*hasher* 是一种实现了 `BuildHasher` trait 的类型。[第十章][traits]会讨论 trait 以及如何实现它们。你不一定要从零开始自己实现 hasher;[crates.io](https://crates.io/) 上有其他 Rust 用户共享的库,它们提供了许多常见哈希算法的 hasher 实现。 [^siphash]: [https://en.wikipedia.org/wiki/SipHash](https://en.wikipedia.org/wiki/SipHash) diff --git a/src/ch09-00-error-handling.md b/src/ch09-00-error-handling.md index 97cbc2df..edd306e3 100644 --- a/src/ch09-00-error-handling.md +++ b/src/ch09-00-error-handling.md @@ -1,10 +1,9 @@ # 错误处理 - - +[ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/13e27c4a35705c4bd473bd90a3d3a8f87ef9ae58/src/ch09-00-error-handling.md) -错误是软件开发中不可避免的事实,所以 Rust 有一些处理出错情况的特性。在许多情况下,Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产环境之前就能发现错误并进行适当的处理。 +错误是软件中的常态,因此 Rust 提供了许多特性来处理某些事情出错的情况。在很多场景下,Rust 会要求你先承认错误发生的可能性,并在代码能够通过编译之前采取一些行动。这项要求会让程序更健壮,因为它确保你会在把代码部署到生产环境之前就发现错误,并妥善处理它们。 -Rust 将错误分为两大类:**可恢复的**(*recoverable*)和 **不可恢复的**(*unrecoverable*)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。 +Rust 将错误分成两大类:**可恢复的**(*recoverable*)和**不可恢复的**(*unrecoverable*)错误。对于可恢复错误,例如“文件未找到”这样的错误,我们多半只想把问题报告给用户,然后重试这次操作。不可恢复错误则总是 bug 的征兆,比如试图访问数组末尾之外的位置,因此我们希望立刻停止程序。 -大多数语言并不区分这两种错误,并采用类似异常(exception)这样方式统一处理它们。Rust 没有异常。相反,它有 `Result` 类型,用于处理可恢复的错误,还有 `panic!` 宏,在程序遇到不可恢复的错误时停止执行。本章首先介绍 `panic!` 调用,接着会讲到如何返回 `Result`。此外,我们将探讨在决定是尝试从错误中恢复还是停止执行时的注意事项。 +大多数语言并不区分这两类错误,而是使用诸如异常(exception)之类的机制以相同方式处理它们。Rust 没有异常。相反,它使用 `Result` 类型来处理可恢复错误,使用 `panic!` 宏在程序遇到不可恢复错误时停止执行。本章会先介绍如何调用 `panic!`,然后再讲如何返回 `Result` 值。此外,我们还会探讨:在决定是尝试从错误中恢复,还是直接停止执行时,需要考虑哪些因素。 diff --git a/src/ch09-01-unrecoverable-errors-with-panic.md b/src/ch09-01-unrecoverable-errors-with-panic.md index 7e4673b6..059d1852 100644 --- a/src/ch09-01-unrecoverable-errors-with-panic.md +++ b/src/ch09-01-unrecoverable-errors-with-panic.md @@ -1,9 +1,8 @@ ## 用 `panic!` 处理不可恢复的错误 - - +[ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/d46785983db2d2f94ca3d571db2cfbad0f5ad3e6/src/ch09-01-unrecoverable-errors-with-panic.md) -突然有一天,代码出问题了,而你对此束手无策。对于这种情况,Rust 有 `panic!`宏。在实践中有两种方法造成 panic:执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者显式调用 `panic!` 宏。这两种情况都会使程序 panic。通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。通过一个环境变量,你也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。 +有时,你的代码里会发生一些糟糕的事情,而且你对此无能为力。在这种情况下,Rust 提供了 `panic!` 宏。实际中有两种方式会导致 panic:一种是执行了会让代码 panic 的操作,比如访问超出数组结尾的位置;另一种是显式调用 `panic!` 宏。这两种情况都会让程序 panic。默认情况下,这些 panic 会打印失败信息、展开栈、清理栈数据,然后退出。你还可以通过环境变量,让 Rust 在 panic 发生时显示调用栈(call stack),以便更容易追踪 panic 的来源。 > ### 响应 panic 时的栈展开或终止 > @@ -36,8 +35,7 @@ ### 使用 `panic!` 的 backtrace - -我们可以使用 `panic!` 被调用的函数的 backtrace 来寻找代码中出问题的地方。下面我们会详细介绍 backtrace 是什么。为了了解如何使用 `panic!` 的 backtrace,让我们来看另一个示例,我们代码中的 bug 引起的别的库中 `panic!` 的例子,而不是直接的宏调用看起来如何。示例 9-1 有一些尝试通过索引访问 vector 中超出有效范围元素的例子: +我们可以利用触发 `panic!` 的函数 backtrace,找出代码里到底是哪一部分出了问题。为了理解如何使用 `panic!` 的 backtrace,让我们再看一个例子:这次 `panic!` 调用不是来自我们直接调用宏,而是因为我们代码里的 bug 触发了库中的 `panic!`。示例 9-1 展示了一段尝试访问 vector 有效索引范围之外元素的代码: 文件名:src/main.rs @@ -49,7 +47,7 @@ 这里尝试访问 vector 的第 100 个元素(这里的索引是 99 因为索引从 0 开始),不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确元素。 -C 语言中,尝试读取数据结构之后的值是未定义行为(undefined behavior)。你会得到任何对应数据结构中这个元素的内存位置的值,甚至是这些内存并不属于这个数据结构的情况。这被称为 **缓存区过读**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数据结构之后未经授权的数据。 +C 语言中,尝试读取数据结构末尾之后的内容属于未定义行为(undefined behavior)。你可能会读到数据结构中对应那个位置的内存里的任意值,即使那块内存根本不属于这个数据结构。这被称为**缓冲区过读**(*buffer overread*),并可能导致安全漏洞;例如,攻击者也许能通过操纵索引,读取本不该被读取、但恰好存储在该数据结构之后的数据。 为了保护程序不受此类漏洞的影响,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。让我们来试一试,看看结果: @@ -57,10 +55,10 @@ C 语言中,尝试读取数据结构之后的值是未定义行为(undefined {{#include ../listings/ch09-error-handling/listing-09-01/output.txt}} ``` -错误指向 *main.rs* 的第 4 行,这里我们试图访问向量 `v` 中的索引 `99`。 +这个错误指向了 *main.rs* 的第 4 行,也就是我们试图访问向量 `v` 中索引 `99` 的地方。 -`note:` 告诉我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace。*backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 `RUST_BACKTRACE` 环境变量设置为任何不是 `0` 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出: +`note:` 这一行告诉我们,可以设置 `RUST_BACKTRACE` 环境变量来得到 backtrace。*backtrace* 是一份到达当前执行点之前所有被调用函数的列表。Rust 中的 backtrace 和其他语言里的工作方式一样:阅读 backtrace 的关键,是从最上面开始往下读,直到看到你自己写的文件。那一处就是问题开始的地方。它上面的那些行,是你的代码调用过的代码;下面的那些行,则是调用了你代码的代码。这些前前后后的行,可能包括 Rust 核心代码、标准库代码,或你正在使用的 crate。现在把 `RUST_BACKTRACE` 环境变量设成除 `0` 之外的任意值,来看看 backtrace。示例 9-2 展示了类似下面这样的输出: ```console $ RUST_BACKTRACE=1 cargo run @@ -88,11 +86,10 @@ note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose bac 示例 9-2:当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息 -这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用调试符号(debug symbols)。当不使用 `--release` 参数运行 cargo build 或 cargo run 时调试符号会默认启用,就像这里一样。 +这里的输出很多!你实际看到的内容可能会因为操作系统和 Rust 版本不同而有所区别。要获得带有这些信息的 backtrace,必须启用调试符号(debug symbols)。当像这里这样,不带 `--release` 参数运行 `cargo build` 或 `cargo run` 时,调试符号默认就是启用的。 示例 9-2 的输出中,backtrace 的第 6 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic,就应当从第一个提到我们自己编写的文件的那一行开始调查。在示例 9-1 中,我们故意编写了会导致 panic 的代码,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免该问题。 本章后面的小节 [“要不要 panic!”][to-panic-or-not-to-panic] 会再次回到 `panic!` 并讲解何时应该、何时不应该使用 `panic!` 来处理错误情况。接下来,我们来看看如何使用 `Result` 来从错误中恢复。 -[to-panic-or-not-to-panic]: -ch09-03-to-panic-or-not-to-panic.html#要不要-panic +[to-panic-or-not-to-panic]: ch09-03-to-panic-or-not-to-panic.html#要不要-panic diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 6571125c..921af808 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -9,7 +9,7 @@ > 我们将要构建的 `MyBox` 类型与真正的 `Box` 有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 `Deref`,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。 -### 追踪指针的值 +### 追踪引用的值 常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-6 中,创建了一个 `i32` 值的引用,接着使用解引用运算符来跟踪所引用的值: