diff --git a/listings/ch08-common-collections/listing-08-04/src/main.rs b/listings/ch08-common-collections/listing-08-04/src/main.rs index abda2db..625ff2c 100755 --- a/listings/ch08-common-collections/listing-08-04/src/main.rs +++ b/listings/ch08-common-collections/listing-08-04/src/main.rs @@ -3,7 +3,7 @@ fn main() { { let v = vec![1, 2, 3, 4]; - // do stuff with v - } // <- v goes out of scope and is freed here + // 处理变量 v + } // <- 这里 v 离开作用域并被丢弃 // ANCHOR_END: here } diff --git a/listings/ch08-common-collections/listing-08-12/src/main.rs b/listings/ch08-common-collections/listing-08-12/src/main.rs index d9e5e76..cd491de 100755 --- a/listings/ch08-common-collections/listing-08-12/src/main.rs +++ b/listings/ch08-common-collections/listing-08-12/src/main.rs @@ -4,7 +4,7 @@ fn main() { let s = data.to_string(); - // the method also works on a literal directly: + // 该方法也可直接用于字符串字面值: let s = "initial contents".to_string(); // ANCHOR_END: here } diff --git a/listings/ch08-common-collections/listing-08-18/src/main.rs b/listings/ch08-common-collections/listing-08-18/src/main.rs index 93939a6..10b5326 100755 --- a/listings/ch08-common-collections/listing-08-18/src/main.rs +++ b/listings/ch08-common-collections/listing-08-18/src/main.rs @@ -2,6 +2,6 @@ fn main() { // ANCHOR: here 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 被移动了,不能继续使用 // ANCHOR_END: here } diff --git a/listings/ch08-common-collections/listing-08-22/src/main.rs b/listings/ch08-common-collections/listing-08-22/src/main.rs index 2b2a73f..7e32cbe 100755 --- a/listings/ch08-common-collections/listing-08-22/src/main.rs +++ b/listings/ch08-common-collections/listing-08-22/src/main.rs @@ -7,7 +7,7 @@ fn main() { 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 不再有效, + // 尝试使用它们看看会出现什么编译错误! // ANCHOR_END: here } diff --git a/src/ch08-00-common-collections.md b/src/ch08-00-common-collections.md index 46d05e0..3bf9d11 100644 --- a/src/ch08-00-common-collections.md +++ b/src/ch08-00-common-collections.md @@ -2,7 +2,7 @@ > [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/main/src/ch08-00-common-collections.md) >
-> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca +> commit 1fd890031311612e54965f7f800a8c8bd4464663 Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。每种集合都有着不同功能和成本,而根据当前情况选择合适的集合,这是一项应当逐渐掌握的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合: @@ -12,6 +12,6 @@ Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常 对于标准库提供的其他类型的集合,请查看[文档][collections]。 -[collections]: https://doc.rust-lang.org/std/collections - 我们将讨论如何创建和更新 vector、字符串和哈希 map,以及它们有什么特别之处。 + +[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 ad458fa..372e288 100644 --- a/src/ch08-01-vectors.md +++ b/src/ch08-01-vectors.md @@ -2,26 +2,26 @@ > [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/main/src/ch08-01-vectors.md) >
-> commit 76df60bccead5f3de96db23d97b69597cd8a2b82 +> commit e7bfb353b107cb150faab9d331c99ea2b91f3725 -我们要讲到的第一个类型是 `Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 +我们要讲到的第一个类型是 `Vec`,也被称为 _vector_。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 ### 新建 vector 为了创建一个新的空 vector,可以调用 `Vec::new` 函数,如示例 8-1 所示: ```rust -let v: Vec = Vec::new(); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-01/src/main.rs:here}} ``` 示例 8-1:新建一个空的 vector 来储存 `i32` 类型的值 -注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。在示例 8-1 中,我们告诉 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`: +通常,我们会用初始值来创建一个 `Vec` 而 Rust 会推断出储存值的类型,所以很少会需要这些类型注解。为了方便 Rust 提供了 `vec!` 宏,这个宏会根据我们提供的值来创建一个新的 vector。示例 8-2 新建一个拥有值 `1`、`2` 和 `3` 的 `Vec`。推断为 `i32` 是因为这是默认整型类型,第三章的 [“数据类型”][data-types] 讨论过: ```rust -let v = vec![1, 2, 3]; +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-02/src/main.rs:here}} ``` 示例 8-2:新建一个包含初值的 vector @@ -33,12 +33,7 @@ let v = vec![1, 2, 3]; 对于新建一个 vector 并向其增加元素,可以使用 `push` 方法,如示例 8-3 所示: ```rust -let mut v = Vec::new(); - -v.push(5); -v.push(6); -v.push(7); -v.push(8); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-03/src/main.rs:here}} ``` 示例 8-3:使用 `push` 方法向 vector 增加值 @@ -50,12 +45,7 @@ v.push(8); 类似于任何其他的 `struct`,vector 在其离开作用域时会被释放,如示例 8-4 所标注的: ```rust -{ - let v = vec![1, 2, 3, 4]; - - // 处理变量 v - -} // <- 这里 v 离开作用域并被丢弃 +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-04/src/main.rs:here}} ``` 示例 8-4:展示 vector 和其元素于何处被丢弃 @@ -69,28 +59,17 @@ v.push(8); 示例 8-5 展示了访问 vector 中一个值的两种方式,索引语法或者 `get` 方法: ```rust -let v = vec![1, 2, 3, 4, 5]; - -let third: &i32 = &v[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."), -} +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-05/src/main.rs:here}} ``` 列表 8-5:使用索引语法或 `get` 方法来访问 vector 中的项 这里有两个需要注意的地方。首先,我们使用索引值 `2` 来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用 `&` 和 `[]` 返回一个引用;或者使用 `get` 方法以索引作为参数来返回一个 `Option<&T>`。 -Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。作为一个例子,让我们看看如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素时程序会如何处理,如示例 8-6 所示: +Rust 提供了两种引用元素的方法的原因是当尝试使用现有元素范围之外的索引值时可以选择让程序如何运行。举个例子,让我们看看使用这个技术,尝试在当有一个 5 个元素的 vector 接着访问索引 100 位置的元素会发生什么,如示例 8-6 所示: ```rust,should_panic,panics -let v = vec![1, 2, 3, 4, 5]; - -let does_not_exist = &v[100]; -let does_not_exist = v.get(100); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-06/src/main.rs:here}} ``` 示例 8-6:尝试访问一个包含 5 个元素的 vector 的索引 100 处的元素 @@ -99,49 +78,30 @@ let does_not_exist = v.get(100); 当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多! -一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第四章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的: +一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第四章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面引用这个元素是行不通的: ```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); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-07/src/main.rs:here}} ``` 示例 8-7:在拥有 vector 中项的引用的同时向其增加一个元素 编译会给出这个错误: -```text -error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable - --> src/main.rs:6:5 - | -4 | let first = &v[0]; - | - immutable borrow occurs here -5 | -6 | v.push(6); - | ^^^^^^^^^ mutable borrow occurs here -7 | -8 | println!("The first element is: {}", first); - | ----- immutable borrow later used here +```console +{{#include ../listings/ch08-common-collections/listing-08-07/output.txt}} ``` 示例 8-7 中的代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。 -> 注意:关于 `Vec` 类型的更多实现细节,在 https://doc.rust-lang.org/stable/nomicon/vec.html 查看 “The Nomicon” +> 注意:关于 `Vec` 类型的更多实现细节,请查看 [“The Rustonomicon”][nomicon] ### 遍历 vector 中的元素 如果想要依次访问 vector 中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。示例 8-8 展示了如何使用 `for` 循环来获取 `i32` 值的 vector 中的每一个元素的不可变引用并将其打印: ```rust -let v = vec![100, 32, 57]; -for i in &v { - println!("{}", i); -} +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-08/src/main.rs:here}} ``` 示例 8-8:通过 `for` 循环遍历 vector 的元素并打印 @@ -149,34 +109,21 @@ for i in &v { 我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们。示例 8-9 中的 `for` 循环会给每一个元素加 `50`: ```rust -let mut v = vec![100, 32, 57]; -for i in &mut v { - *i += 50; -} +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-09/src/main.rs:here}} ``` -示例8-9:遍历 vector 中元素的可变引用 +示例 8-9:遍历 vector 中元素的可变引用 为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。第十五章的 [“通过解引用运算符追踪指针的值”][deref] 部分会详细介绍解引用运算符。 ### 使用枚举来储存多种类型 -在本章的开始,我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举! +vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举! 例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了。示例 8-10 展示了其用例: ```rust -enum SpreadsheetCell { - Int(i32), - Float(f64), - Text(String), -} - -let row = vec![ - SpreadsheetCell::Int(3), - SpreadsheetCell::Text(String::from("blue")), - SpreadsheetCell::Float(10.12), -]; +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-10/src/main.rs:here}} ``` 示例 8-10:定义一个枚举,以便能在 vector 中存放不同类型的数据 @@ -185,6 +132,9 @@ Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需 如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。 -现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中 `Vec` 定义的很多其他实用方法的 API 文档。例如,除了 `push` 之外还有一个 `pop` 方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`! +现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中 `Vec` 定义的很多其他实用方法的 [API 文档][vec-api]。例如,除了 `push` 之外还有一个 `pop` 方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`! -[deref]: ch15-02-deref.html#following-the-pointer-to-the-value-with-the-dereference-operator +[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#通过解引用运算符追踪指针的值 diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index 2eb5856..4880904 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -2,7 +2,7 @@ > [ch08-02-strings.md](https://github.com/rust-lang/book/blob/main/src/ch08-02-strings.md) >
-> commit c084bdd9ee328e7e774df19882ccc139532e53d8 +> commit db403a8bdfe5223d952737f54b0d9651b3e6ae1d 第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的程序员就可能显得很困难了。 @@ -21,7 +21,7 @@ Rust 标准库中还包含一系列其他字符串类型,比如 `OsString`、` 很多 `Vec` 可用的操作在 `String` 中同样可用,从以 `new` 函数创建字符串开始,如示例 8-11 所示。 ```rust -let mut s = String::new(); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-11/src/main.rs:here}} ``` 示例 8-11:新建一个空的 `String` @@ -29,12 +29,7 @@ let mut s = String::new(); 这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,字符串字面值也实现了它。示例 8-12 展示了两个例子。 ```rust -let data = "initial contents"; - -let s = data.to_string(); - -// 该方法也可直接用于字符串字面值: -let s = "initial contents".to_string(); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-12/src/main.rs:here}} ``` 示例 8-12:使用 `to_string` 方法从字符串字面值创建 `String` @@ -44,27 +39,17 @@ let s = "initial contents".to_string(); 也可以使用 `String::from` 函数来从字符串字面值创建 `String`。示例 8-13 中的代码等同于使用 `to_string`。 ```rust -let s = String::from("initial contents"); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-13/src/main.rs:here}} ``` 示例 8-13:使用 `String::from` 函数从字符串字面值创建 `String` -因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择。其中一些可能看起来多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是风格问题了。 +因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择。其中一些可能看起来多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是代码风格与可读性的问题了。 记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据,如示例 8-14 所示。 ```rust -let hello = String::from("السلام عليكم"); -let hello = String::from("Dobrý den"); -let hello = String::from("Hello"); -let hello = String::from("שָׁלוֹם"); -let hello = String::from("नमस्ते"); -let hello = String::from("こんにちは"); -let hello = String::from("안녕하세요"); -let hello = String::from("你好"); -let hello = String::from("Olá"); -let hello = String::from("Здравствуйте"); -let hello = String::from("Hola"); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:here}} ``` 示例 8-14:在字符串中储存不同语言的问候语 @@ -80,30 +65,25 @@ let hello = String::from("Hola"); 可以通过 `push_str` 方法来附加字符串 slice,从而使 `String` 变长,如示例 8-15 所示。 ```rust -let mut s = String::from("foo"); -s.push_str("bar"); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-15/src/main.rs:here}} ``` 示例 8-15:使用 `push_str` 方法向 `String` 附加字符串 slice -执行这两行代码之后,`s` 将会包含 `foobar`。`push_str` 方法采用字符串 slice,因为我们并不需要获取参数的所有权。例如,示例 8-16 展示了如果将 `s2` 的内容附加到 `s1` 之后,自身不能被使用就糟糕了。 +执行这两行代码之后,`s` 将会包含 `foobar`。`push_str` 方法采用字符串 slice,因为我们并不需要获取参数的所有权。例如,示例 8-16 中我们希望在将 `s2` 的内容附加到 `s1` 之后还能使用它。 ```rust -let mut s1 = String::from("foo"); -let s2 = "bar"; -s1.push_str(s2); -println!("s2 is {}", s2); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-16/src/main.rs:here}} ``` 示例 8-16:将字符串 slice 的内容附加到 `String` 后使用它 如果 `push_str` 方法获取了 `s2` 的所有权,就不能在最后一行打印出其值了。好在代码如我们期望那样工作! -`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 *l* 加入 `String` 的代码。 +`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 "l" 加入 `String` 的代码。 ```rust -let mut s = String::from("lo"); -s.push('l'); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-17/src/main.rs:here}} ``` 示例 8-17:使用 `push` 将一个字符加入 `String` 值中 @@ -115,9 +95,7 @@ s.push('l'); 通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用 `+` 运算符,如示例 8-18 所示。 ```rust -let s1 = String::from("Hello, "); -let s2 = String::from("world!"); -let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用 +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-18/src/main.rs:here}} ``` 示例 8-18:使用 `+` 运算符将两个 `String` 值合并到一个新的 `String` 值中 @@ -139,46 +117,31 @@ fn add(self, s: &str) -> String { 如果想要级联多个字符串,`+` 的行为就显得笨重了: ```rust -let s1 = String::from("tic"); -let s2 = String::from("tac"); -let s3 = String::from("toe"); - -let s = s1 + "-" + &s2 + "-" + &s3; +{{#rustdoc_include ../listings/ch08-common-collections/no-listing-01-concat-multiple-strings/src/main.rs:here}} ``` 这时 `s` 的内容会是 “tic-tac-toe”。在有这么多 `+` 和 `"` 字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用 `format!` 宏: ```rust -let s1 = String::from("tic"); -let s2 = String::from("tac"); -let s3 = String::from("toe"); - -let s = format!("{}-{}-{}", s1, s2, s3); +{{#rustdoc_include ../listings/ch08-common-collections/no-listing-02-format/src/main.rs:here}} ``` -这些代码也会将 `s` 设置为 “tic-tac-toe”。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 `String`。这个版本就好理解的多,并且不会获取任何参数的所有权。 +这些代码也会将 `s` 设置为 “tic-tac-toe”。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 `String`。这个版本就好理解的多,宏 `format!` 生成的代码使用索引并且不会获取任何参数的所有权。 ### 索引字符串 在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 `String` 的一部分,会出现一个错误。考虑一下如示例 8-19 中所示的无效代码。 ```rust,ignore,does_not_compile -let s1 = String::from("hello"); -let h = s1[0]; +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-19/src/main.rs:here}} ``` 示例 8-19:尝试对字符串使用索引语法 这段代码会导致如下错误: -```text -error[E0277]: the trait bound `std::string::String: std::ops::Index<{integer}>` is not satisfied - --> - | -3 | let h = s1[0]; - | ^^^^^ the type `std::string::String` cannot be indexed by `{integer}` - | - = help: the trait `std::ops::Index<{integer}>` is not implemented for `std::string::String` +```console +{{#include ../listings/ch08-common-collections/listing-08-19/output.txt}} ``` 错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。 @@ -188,13 +151,13 @@ error[E0277]: the trait bound `std::string::String: std::ops::Index<{integer}>` `String` 是一个 `Vec` 的封装。让我们看看示例 8-14 中一些正确编码的字符串的例子。首先是这一个: ```rust -let len = String::from("Hola").len(); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:spanish}} ``` 在这里,`len` 的值是 4 ,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 。) ```rust -let len = String::from("Здравствуйте").len(); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:russian}} ``` 当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码: @@ -204,7 +167,9 @@ let hello = "Здравствуйте"; let answer = &hello[0]; ``` -`answer` 的值应该是什么呢?它应该是第一个字符 `З` 吗?当使用 UTF-8 编码时,`З` 的第一个字节 `208`,第二个是 `151`,所以 `answer` 实际上应该是 `208`,不过 `208` 自身并不是一个有效的字母。返回 `208` 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回,即便这个字符串只有拉丁字母: 即便 `&"hello"[0]` 是返回字节值的有效代码,它也应当返回 `104` 而不是 `h`。为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。 +我们已经知道 `answer` 不是第一个字符 `З`。当使用 UTF-8 编码时,`З` 的第一个字节 `208`,第二个是 `151`,所以 `answer` 实际上应该是 `208`,不过 `208` 自身并不是一个有效的字母。返回 `208` 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回,即便这个字符串只有拉丁字母: 即便 `&"hello"[0]` 是返回字节值的有效代码,它也应当返回 `104` 而不是 `h`。 + +为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。 #### 字节、标量值和字形簇!天呐! @@ -213,7 +178,8 @@ let answer = &hello[0]; 比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 `u8` 值看起来像这样: ```text -[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135] +[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, +224, 165, 135] ``` 这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 `char` 类型那样,这些字节看起来像这样: @@ -246,17 +212,15 @@ let s = &hello[0..4]; 如果获取 `&hello[0..1]` 会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样: -```text -thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4 +```console +{{#include ../listings/ch08-common-collections/output-only-01-not-char-boundary/output.txt}} ``` 你应该小心谨慎的使用这个操作,因为这么做可能会使你的程序崩溃。 ### 遍历字符串的方法 -幸运的是,这里还有其他获取字符串元素的方式。 - -如果你需要操作单独的 Unicode 标量值,最好的选择是使用 `chars` 方法。对 “नमस्ते” 调用 `chars` 方法会将其分开并返回六个 `char` 类型的值,接着就可以遍历其结果来访问每一个元素了: +操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 `chars` 方法。对 “नमस्ते” 调用 `chars` 方法会将其分开并返回六个 `char` 类型的值,接着就可以遍历其结果来访问每一个元素了: ```rust for c in "नमस्ते".chars() { @@ -275,7 +239,7 @@ for c in "नमस्ते".chars() { े ``` -`bytes` 方法返回每一个原始字节,这可能会适合你的使用场景: +另外 `bytes` 方法返回每一个原始字节,这可能会适合你的使用场景: ```rust for b in "नमस्ते".bytes() { @@ -295,7 +259,7 @@ for b in "नमस्ते".bytes() { 不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。 -从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。[crates.io](https://crates.io) 上有些提供这样功能的 crate。 +从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。[crates.io](https://crates.io/) 上有些提供这样功能的 crate。 ### 字符串并不简单 diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index 8480880..7745e47 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -2,7 +2,7 @@ > [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/main/src/ch08-03-hash-maps.md) >
-> commit 85b02530cc749565c26c05bf1b3a838334e9717f +> commit 1fd890031311612e54965f7f800a8c8bd4464663 最后介绍的常用集合类型是 **哈希 map**(*hash map*)。`HashMap` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个 **哈希函数**(*hashing function*)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。 @@ -15,12 +15,7 @@ 可以使用 `new` 创建一个空的 `HashMap`,并使用 `insert` 增加元素。在示例 8-20 中我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分: ```rust -use std::collections::HashMap; - -let mut scores = HashMap::new(); - -scores.insert(String::from("Blue"), 10); -scores.insert(String::from("Yellow"), 50); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-20/src/main.rs:here}} ``` 示例 8-20:新建一个哈希 map 并插入一些键值对 @@ -29,35 +24,22 @@ scores.insert(String::from("Yellow"), 50); 像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 `HashMap` 的键类型是 `String` 而值类型是 `i32`。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。 -另一个构建哈希 map 的方法是使用一个元组的 vector 的 `collect` 方法,其中每个元组包含一个键值对。`collect` 方法可以将数据收集进一系列的集合类型,包括 `HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 `zip` 方法来创建一个元组的 vector,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 `collect` 方法将这个元组 vector 转换成一个 `HashMap`,如示例 8-21 所示: +另一个构建哈希 map 的方法是在一个元组的 vector 上使用迭代器(iterator)和 `collect` 方法,其中每个元组包含一个键值对。我们会在[第十三章的 “Processing a Series of Items with Iterators” 部分][iterators] 介绍迭代器及其关联方法。`collect` 方法可以将数据收集进一系列的集合类型,包括 `HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 `zip` 方法来创建一个元组的迭代器,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 `collect` 方法将这个元组的迭代器转换成一个 `HashMap`,如示例 8-21 所示: ```rust -use std::collections::HashMap; - -let teams = vec![String::from("Blue"), String::from("Yellow")]; -let initial_scores = vec![10, 50]; - -let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-21/src/main.rs:here}} ``` 示例 8-21:用队伍列表和分数列表创建哈希 map -这里 `HashMap<_, _>` 类型注解是必要的,因为可能 `collect` 为很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。 +这里 `HashMap<_, _>` 类型注解是必要的,因为可能 `collect` 为很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。在示例 8-21 中,键(key)类型是 `String`,值(value)类型是 `i32`,与示例 8-20 的类型一样。 ### 哈希 map 和所有权 对于像 `i32` 这样的实现了 `Copy` trait 的类型,其值可以拷贝进哈希 map。对于像 `String` 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者,如示例 8-22 所示: ```rust -use std::collections::HashMap; - -let field_name = String::from("Favorite color"); -let field_value = String::from("Blue"); - -let mut map = HashMap::new(); -map.insert(field_name, field_value); -// 这里 field_name 和 field_value 不再有效, -// 尝试使用它们看看会出现什么编译错误! +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-22/src/main.rs:here}} ``` 示例 8-22:展示一旦键值对被插入后就为哈希 map 所拥有 @@ -71,15 +53,7 @@ map.insert(field_name, field_value); 可以通过 `get` 方法并提供对应的键来从哈希 map 中获取值,如示例 8-23 所示: ```rust -use std::collections::HashMap; - -let mut scores = HashMap::new(); - -scores.insert(String::from("Blue"), 10); -scores.insert(String::from("Yellow"), 50); - -let team_name = String::from("Blue"); -let score = scores.get(&team_name); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-23/src/main.rs:here}} ``` 示例 8-23:访问哈希 map 中储存的蓝队分数 @@ -89,16 +63,7 @@ let score = scores.get(&team_name); 可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 `for` 循环: ```rust -use std::collections::HashMap; - -let mut scores = HashMap::new(); - -scores.insert(String::from("Blue"), 10); -scores.insert(String::from("Yellow"), 50); - -for (key, value) in &scores { - println!("{}: {}", key, value); -} +{{#rustdoc_include ../listings/ch08-common-collections/no-listing-03-iterate-over-hashmap/src/main.rs:here}} ``` 这会以任意顺序打印出每一个键值对: @@ -117,14 +82,7 @@ Blue: 10 如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便示例 8-24 中的代码调用了两次 `insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值: ```rust -use std::collections::HashMap; - -let mut scores = HashMap::new(); - -scores.insert(String::from("Blue"), 10); -scores.insert(String::from("Blue"), 25); - -println!("{:?}", scores); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-24/src/main.rs:here}} ``` 示例 8-24:替换以特定键储存的值 @@ -136,15 +94,7 @@ println!("{:?}", scores); 我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像示例 8-25 这样: ```rust -use std::collections::HashMap; - -let mut scores = HashMap::new(); -scores.insert(String::from("Blue"), 10); - -scores.entry(String::from("Yellow")).or_insert(50); -scores.entry(String::from("Blue")).or_insert(50); - -println!("{:?}", scores); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-25/src/main.rs:here}} ``` 示例 8-25:使用 `entry` 方法只在键没有对应一个值时插入 @@ -158,35 +108,24 @@ println!("{:?}", scores); 另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,示例 8-26 中的代码计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 `0`。 ```rust -use std::collections::HashMap; - -let text = "hello world wonderful world"; - -let mut map = HashMap::new(); - -for word in text.split_whitespace() { - let count = map.entry(word).or_insert(0); - *count += 1; -} - -println!("{:?}", map); +{{#rustdoc_include ../listings/ch08-common-collections/listing-08-26/src/main.rs:here}} ``` 示例 8-26:通过哈希 map 储存单词和计数来统计出现次数 -这会打印出 `{"world": 2, "hello": 1, "wonderful": 1}`,`or_insert` 方法事实上会返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在 `count` 变量中,所以为了赋值必须首先使用星号(`*`)解引用 `count`。这个可变引用在 `for` 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。 +这会打印出 `{"world": 2, "hello": 1, "wonderful": 1}`。`split_whitespace` 方法会迭代 `text` 的值由空格分隔的子 slice。`or_insert` 方法返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在 `count` 变量中,所以为了赋值必须首先使用星号(`*`)解引用 `count`。这个可变引用在 `for` 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。 ### 哈希函数 -`HashMap` 默认使用一种 “密码学安全的”(“cryptographically strong” )[^siphash] 哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 +`HashMap` 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)[^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) +[^siphash]: [https://en.wikipedia.org/wiki/SipHash](https://en.wikipedia.org/wiki/SipHash) ## 总结 vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题: -* 给定一系列数字,使用 vector 并返回这个列表的平均数(mean, average)、中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希 map 会很有帮助)。 +* 给定一系列数字,使用 vector 并返回这个列表的中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希 map 会很有帮助)。 * 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 “ay”,所以 “first” 会变成 “irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple” 会变成 “apple-hay”)。牢记 UTF-8 编码! * 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字典序排列的列表。 @@ -194,5 +133,6 @@ vector、字符串和哈希 map 会在你的程序需要储存、访问和修改 我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机! +[iterators]: ch13-02-iterators.html [validating-references-with-lifetimes]: -ch10-03-lifetime-syntax.html#validating-references-with-lifetimes +ch10-03-lifetime-syntax.html#生命周期与引用有效性