diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index 80f965e..db4dbda 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -4,29 +4,31 @@ >
> commit c2fd7b2d39c4130dd17bb99c101ac94af83d1a44 -第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。 +第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。 字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引` String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。 ### 什么是字符串? -在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slice**:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。 +在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slice**:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。 -称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 `String` 和字符串 slice `&str`类型,而不是其中一个。这一部分大部分是关于 `String` 的,不过这些类型在 Rust 标准库中都被广泛使用。`String` 和字符串 slice 都是 UTF-8 编码的。 +称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 `String` 和字符串 slice `&str`类型,而不仅仅是其中之一。虽然本部分内容大多是关于 `String` 的,不过这两个类型在 Rust 标准库中都被广泛使用,`String` 和字符串 slice 都是 UTF-8 编码的。 Rust 标准库中还包含一系列其他字符串类型,比如 `OsString`、`OsStr`、`CString` 和 `CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。与 `*String`/`*Str` 的命名类似,它们通常也提供有所有权和可借用的变体,就比如说 `String`/`&str`。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用它们以及各自适合的场景。 ### 新建字符串 -很多 `Vec` 可用的操作在 `String` 中同样可用,从以 `new` 函数创建字符串开始,像这样: +很多 `Vec` 可用的操作在 `String` 中同样可用,从以 `new` 函数创建字符串开始,如示例 8-11 所示: ```rust let mut s = String::new(); ``` +示例 8-11:新建一个空的 `String` + 这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。 -通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,对于字符串字面值是这样: +通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,字符串字面值就可以。示例 8-12 展示了两个例子: ```rust let data = "initial contents"; @@ -37,65 +39,82 @@ let s = data.to_string(); let s = "initial contents".to_string(); ``` -这会创建一个包含 `initial contents` 的字符串。 +示例 8-12:使用 `to_string` 方法从字符串字面值创建 `String` -也可以使用 `String::from` 函数来从字符串字面值创建 `String`。下面代码等同于使用 `to_string`: +这些代码会创建包含 `initial contents` 的字符串。 + +也可以使用 `String::from` 函数来从字符串字面值创建 `String`。示例 8-13 中的代码代码等同于使用 `to_string`: ```rust let s = String::from("initial contents"); ``` -因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。它们有些可能显得有些多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是风格问题了。 +示例 8-13:使用 `String::from` 函数从字符串字面值创建 `String` -记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据: +因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择。它们有些可能显得有些多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是风格问题了。 + +记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据,如示例 8-14 所示: ```rust -let hello = "السلام عليكم"; -let hello = "Dobrý den"; -let hello = "Hello"; -let hello = "שָׁלוֹם"; -let hello = "नमस्ते"; -let hello = "こんにちは"; -let hello = "안녕하세요"; -let hello = "你好"; -let hello = "Olá"; -let hello = "Здравствуйте"; -let hello = "Hola"; +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"); ``` +示例 8-14:在字符串中储存不同语言的问候语 + +所有这些都是有效的 `String`值。 + ### 更新字符串 -`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,`String` 实现了 `+` 运算符作为级联运算符以便于使用。 +`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,`String` 实现了 `+` 运算符作为连接运算符以便于使用。 #### 使用 push 附加字符串 -可以通过 `push_str` 方法来附加字符串 slice,从而使 `String` 变长: +可以通过 `push_str` 方法来附加字符串 slice,从而使 `String` 变长,如示例 8-15 所示: ```rust let mut s = String::from("foo"); s.push_str("bar"); ``` -执行这两行代码之后 `s` 将会包含 “foobar”。`push_str` 方法获取字符串 slice,因为我们并不需要获取参数的所有权。例如,如果将 `s2` 的内容附加到 `s1` 中后自身不能被使用就糟糕了: +示例 8-15:使用 `push_str` 方法向 `String` 附加字符串 slice + +执行这两行代码之后 `s` 将会包含 `foobar`。`push_str` 方法获取字符串 slice,因为我们并不需要获取参数的所有权。例如,示例 8-16 展示了如果将 `s2` 的内容附加到 `s1` 中后自身不能被使用就糟糕了: ```rust let mut s1 = String::from("foo"); -let s2 = String::from("bar"); +let s2 = "bar"; s1.push_str(&s2); +println!("s2 is {}", s2); ``` -`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中: +示例 8-16:将字符串 slice 的内容附加到 `String` 后使用它 + +如果 `push_str` 方法获取了 `s2` 的所有权,就不能在最后一行打印出其值了。好在代码如我们期望那样工作! + +`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 l 加入 `String` 的代码: ```rust let mut s = String::from("lo"); s.push('l'); ``` +示例 8-17:使用 `push` 将一个字符加入 `String` 值中 + 执行这些代码之后,`s` 将会包含 “lol”。 -#### 使用 + 运算符或 `format!` 宏级联字符串 +#### 使用 + 运算符或 `format!` 宏连接字符串 -通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用 `+` 运算符: +通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用 `+` 运算符,如示例 8-18 所示: ```rust let s1 = String::from("Hello, "); @@ -103,15 +122,19 @@ let s2 = String::from("world!"); let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be used ``` +示例 8-18:使用 `+` 运算符将两个 `String` 值合并到一个新的 `String` 值中 + 执行完这些代码之后字符串 `s3` 将会包含 `Hello, world!`。`s1` 在相加后不再有效的原因,和使用 `s2` 的引用的原因与使用 `+` 运算符时调用的方法签名有关,这个函数签名看起来像这样: ```rust,ignore fn add(self, s: &str) -> String { ``` -这并不是标准库中实际的签名;那个 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那奇怪的部分的线索。 +这并不是标准库中实际的签名;标准库中的 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那微妙部分的线索。 -首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下——正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么代码还能编译呢?之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被**强转**(*coerced*)成 `&str`——当`add`函数被调用时,Rust 使用了一个被称为 **解引用强制多态**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]` 以供 `add` 函数使用。第十五章会更深入的讨论解引用强制多态。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。 +首先,`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` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着上面例子中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。 @@ -135,31 +158,34 @@ let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); ``` -这些代码也会将 `s` 设置为 “tic-tac-toe”。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的 `String`。这个版本就好理解的多,并且不会获取任何参数的所有权。 +这些代码也会将 `s` 设置为 “tic-tac-toe”。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 `String`。这个版本就好理解的多,并且不会获取任何参数的所有权。 ### 索引字符串 -在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问 `String` 的一部分,会出现一个错误。比如如下代码: +在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问 `String` 的一部分,会出现一个错误。考虑一下如示例 8-19 中所示的无效代码: ```rust,ignore let s1 = String::from("hello"); let h = s1[0]; ``` +示例 8-19:尝试对字符串使用索引语法 + 会导致如下错误: ```text -error: the trait bound `std::string::String: std::ops::Index<_>` is not -satisfied [--explain E0277] - |> - |> let h = s1[0]; - |> ^^^^^ -note: the type `std::string::String` cannot be indexed by `_` +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` ``` 错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。 -#### 内部表示 +#### 内部表现 `String` 是一个 `Vec` 的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个: @@ -167,22 +193,20 @@ note: the type `std::string::String` cannot be indexed by `_` let len = String::from("Hola").len(); ``` -在这里,`len` 的值是四,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢? +在这里,`len` 的值是四,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢? ```rust let len = String::from("Здравствуйте").len(); ``` -当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。 - -作为演示,考虑如下无效的 Rust 代码: +当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码: ```rust,ignore let hello = "Здравствуйте"; let answer = &hello[0]; ``` -`answer` 的值应该是什么呢?它应该是第一个字符 `З` 吗?当使用 UTF-8 编码时,`З` 的第一个字节 `208`,第二个是 `151`,所以 `answer` 实际上应该是 `208`,不过 `208` 自身并不是一个有效的字母。返回 `208` 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:`&"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 +237,7 @@ Rust 提供了多种不同的方式来解释计算机储存的原始字符串数 ### 字符串 slice -因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用 `[]` 和单个值的索引更加明确的方式是使用 `[]` 和一个 range 来创建包含特定字节的字符串 slice: +索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时 Rust 会要求你更明确一些。为了更明确索引并表民你需要一个字符串 slice,相比使用 `[]` 和单个值的索引,可以使用 `[]` 和一个 range 来创建含特定字节的字符串 slice: ```rust let hello = "Здравствуйте"; @@ -221,13 +245,12 @@ let hello = "Здравствуйте"; let s = &hello[0..4]; ``` -这里,`s` 是一个 `&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 `s` 将会是 “Зд”。 +这里,`s` 会是一个 `&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 `s` 将会是 “Зд”。 如果获取 `&hello[0..1]` 会发生什么呢?答案是:在运行时会 panic,就跟访问 vector 中的无效索引时一样: ```text -thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on -character boundary', ../src/libcore/str/mod.rs:1694 +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 ``` 你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。 @@ -236,7 +259,7 @@ character boundary', ../src/libcore/str/mod.rs:1694 幸运的是,这里还有其他获取字符串元素的方式。 -如果你需要操作单独的 Unicode 标量值,最好的选择是使用 `chars` 方法。对 “नमस्ते” 调用 `chars` 方法会将其分开并返回六个 `char` 类型的值,接着就可以遍历结果来访问每一个元素了: +如果我们需要操作单独的 Unicode 标量值,最好的选择是使用 `chars` 方法。对 “नमस्ते” 调用 `chars` 方法会将其分开并返回六个 `char` 类型的值,接着就可以遍历其结果来访问每一个元素了: ```rust for c in "नमस्ते".chars() { @@ -275,10 +298,10 @@ for b in "नमस्ते".bytes() { 不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。 -从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。 +从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。[crates.io](https://crates.io) 上有些提供这样功能的 crate。 ### 字符串并不简单 -总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。 +总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期后期免于处理涉及非 ASCII 字符的错误。 现在让我们转向一些不太复杂的集合:哈希 map! diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index d93aa11..8213d07 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -1,8 +1,8 @@ -## 哈希 map +## 哈希 map 储存键值对 > [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit c2fd7b2d39c4130dd17bb99c101ac94af83d1a44 最后介绍的常用集合类型是 **哈希 map**(*hash map*)。`HashMap` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个 **哈希函数**(*hashing function*)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。 @@ -23,10 +23,13 @@ scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); ``` +示例 8-20:新建一个哈希 map 并插入一些键值对 + 注意必须首先 `use` 标准库中集合部分的 `HashMap`。在这三个常用集合中,`HashMap` 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 `HashMap` 的支持也相对较少,例如,并没有内建的构建宏。 + 像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 `HashMap` 的键类型是 `String` 而值类型是 `i32`。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。 -另一个构建哈希 map 的方法是使用一个元组的 vector 的 `collect` 方法,其中每个元组包含一个键值对。`collect` 方法可以将数据收集进一系列的集合类型,包括 `HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 `zip` 方法来创建一个元组的 vector,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 `collect` 方法将这个元组 vector 转换成一个 `HashMap`: +另一个构建哈希 map 的方法是使用一个元组的 vector 的 `collect` 方法,其中每个元组包含一个键值对。`collect` 方法可以将数据收集进一系列的集合类型,包括 `HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 `zip` 方法来创建一个元组的 vector,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 `collect` 方法将这个元组 vector 转换成一个 `HashMap`,如示例 8-21 所示: ```rust use std::collections::HashMap; @@ -37,11 +40,13 @@ let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); ``` +示例 8-21:用队伍列表和分数列表创建哈希 map + 这里 `HashMap<_, _>` 类型注解是必要的,因为可能 `collect` 很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。 ### 哈希 map 和所有权 -对于像 `i32` 这样的实现了 `Copy` trait 的类型,其值可以拷贝进哈希 map。对于像 `String` 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者: +对于像 `i32` 这样的实现了 `Copy` trait 的类型,其值可以拷贝进哈希 map。对于像 `String` 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者,如示例 8-22 所示: ```rust use std::collections::HashMap; @@ -51,16 +56,19 @@ 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 +// field_name and field_value are invalid at this point, try using them and +// see what compiler error you get! ``` +示例 8-22:展示一旦键值对被插入后就为哈希 map 所拥有 + 当 `insert` 调用将 `field_name` 和 `field_value` 移动到哈希 map 中后,将不能使用这两个绑定。 -如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。 +如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章 “使用生命周期保证引用有效” 部分将会更多的讨论这个问题。 ### 访问哈希 map 中的值 -可以通过 `get` 方法并提供对应的键来从哈希 map 中获取值: +可以通过 `get` 方法并提供对应的键来从哈希 map 中获取值,如示例 8-23 所示: ```rust use std::collections::HashMap; @@ -74,7 +82,9 @@ let team_name = String::from("Blue"); let score = scores.get(&team_name); ``` -这里,`score` 是与蓝队分数相关的值,应为 `Some(10)`。因为 `get` 返回 `Option`,所以结果被装进 `Some`;如果某个键在哈希 map 中没有对应的值,`get` 会返回 `None`。这时就要用某种第六章提到的方法来处理 `Option`。 +示例 8-23:访问哈希 map 中储存的蓝队分数 + +这里,`score` 是与蓝队分数相关的值,应为 `Some(10)`。因为 `get` 返回 `Option`,所以结果被装进 `Some`;如果某个键在哈希 map 中没有对应的值,`get` 会返回 `None`。这时就要用某种第六章提到的方法之一来处理 `Option`。 可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 `for` 循环: @@ -104,7 +114,7 @@ Blue: 10 #### 覆盖一个值 -如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次 `insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值: +如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便示例 8-24 中的代码调用了两次 `insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值: ```rust use std::collections::HashMap; @@ -117,11 +127,13 @@ scores.insert(String::from("Blue"), 25); println!("{:?}", scores); ``` -这会打印出 `{"Blue": 25}`。原始的值 10 将被覆盖。 +示例 8-24:替换以特定键储存的值 + +这会打印出 `{"Blue": 25}`。原始的值 `10` 则被覆盖了。 #### 只在键没有对应值时插入 -我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样: +我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像示例 8-25 这样: ```rust use std::collections::HashMap; @@ -135,14 +147,15 @@ scores.entry(String::from("Blue")).or_insert(50); println!("{:?}", scores); ``` +示例 8-25:使用 `entry` 方法只在键没有对应一个值时插入 + `Entry` 的 `or_insert` 方法在键对应的值存在时就返回这个值的 `Entry`,如果不存在则将参数作为新值插入并返回修改过的 `Entry`。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。 -这段代码会打印出 `{"Yellow": 50, "Blue": 10}`。第一个 `entry` 调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个 `entry` 调用不会改变哈希 map 因为蓝队已经有了值 10。 +运行示例 8-25 的代码会打印出 `{"Yellow": 50, "Blue": 10}`。第一个 `entry` 调用会插入黄队的键和值 `50`,因为黄队并没有一个值。第二个 `entry` 调用不会改变哈希 map 因为蓝队已经有了值 `10`。 #### 根据旧值更新一个值 -另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 `0`。 - +另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,示例 8-26 中的代码计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 `0`。 ```rust use std::collections::HashMap; @@ -159,11 +172,13 @@ for word in text.split_whitespace() { println!("{:?}", map); ``` +示例 8-26:通过哈希 map 储存单词和计数来统计出现次数 + 这会打印出 `{"world": 2, "hello": 1, "wonderful": 1}`,`or_insert` 方法事实上会返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在 `count` 变量中,所以为了赋值必须首先使用星号(`*`)解引用 `count`。这个可变引用在 `for` 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。 ### 哈希函数 -`HashMap` 默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 +`HashMap` 默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 ## 总结 diff --git a/src/ch09-00-error-handling.md b/src/ch09-00-error-handling.md index 609d6ee..791e484 100644 --- a/src/ch09-00-error-handling.md +++ b/src/ch09-00-error-handling.md @@ -2,9 +2,9 @@ > [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-00-error-handling.md) >
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit a764530433720fe09ae2d97874c25341f8322573 -Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以当出现错误时, Rust 有很多特性来处理当前情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。 +Rust 对可靠性的执着也延伸到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多特性来处理出现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。 Rust 将错误组合成两个主要类别:**可恢复错误**(*recoverable*)和 **不可恢复错误**(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。 diff --git a/src/ch09-01-unrecoverable-errors-with-panic.md b/src/ch09-01-unrecoverable-errors-with-panic.md index 8d316c2..64c426a 100644 --- a/src/ch09-01-unrecoverable-errors-with-panic.md +++ b/src/ch09-01-unrecoverable-errors-with-panic.md @@ -2,7 +2,7 @@ > [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) >
-> commit 8d24b2a5e61b4eea109d26e38d2144408ae44e53 +> commit a764530433720fe09ae2d97874c25341f8322573 突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。 @@ -32,18 +32,17 @@ $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.25 secs Running `target/debug/panic` -thread 'main' panicked at 'crash and burn', src/main.rs:2 +thread 'main' panicked at 'crash and burn', src/main.rs:2:4 note: Run with `RUST_BACKTRACE=1` for a backtrace. -error: Process didn't exit successfully: `target/debug/panic` (exit code: 101) ``` -最后三行包含 `panic!` 造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2* 表明这是 *src/main.rs* 文件的第二行。 +最后三行包含 `panic!` 造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:4* 表明这是 *src/main.rs* 文件的第二行第四个字符。 -在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 `panic!` 宏的调用。换句话说,`panic!` 可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。可以使用 `panic!` 被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。 +在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 `panic!` 宏的调用。在其他情况下,`panic!` 可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。可以使用 `panic!` 被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。下面我们会详细介绍 backtrace 是什么。 ### 使用 `panic!` 的 backtrace -让我们来看看另一个因为我们代码中的 bug 引起的别的库中 `panic!` 的例子,而不是直接的宏调用: +让我们来看看另一个因为我们代码中的 bug 引起的别的库中 `panic!` 的例子,而不是直接的宏调用。示例 9-1 有一些尝试通过索引访问 vector 中元素的例子: 文件名: src/main.rs @@ -51,10 +50,12 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101) fn main() { let v = vec![1, 2, 3]; - v[100]; + v[99]; } ``` +示例 9-1:尝试访问超越 vector 结尾的元素,这会造成 `panic!` + 这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。 这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 **缓冲区溢出**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。 @@ -67,59 +68,61 @@ $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs Running `target/debug/panic` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is -100', /stable-dist-rustc/build/src/libcollections/vec.rs:1362 +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` 的实现。这是当对 vector `v` 使用 `[]` 时 *libcollections/vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。 -接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。示例 9-1 显示了其输出: +接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace *backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们尝试获取一个 backtrace:示例 9-2 展示了与你看到类似的输出: ```text $ RUST_BACKTRACE=1 cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/panic` -thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', /stable-dist-rustc/build/src/libcollections/vec.rs:1392 +thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /checkout/src/liballoc/vec.rs:1555:10 stack backtrace: - 1: 0x560ed90ec04c - std::sys::imp::backtrace::tracing::imp::write::hf33ae72d0baa11ed - at /stable-dist-rustc/build/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:42 - 2: 0x560ed90ee03e - std::panicking::default_hook::{{closure}}::h59672b733cc6a455 - at /stable-dist-rustc/build/src/libstd/panicking.rs:351 - 3: 0x560ed90edc44 - std::panicking::default_hook::h1670459d2f3f8843 - at /stable-dist-rustc/build/src/libstd/panicking.rs:367 - 4: 0x560ed90ee41b - std::panicking::rust_panic_with_hook::hcf0ddb069e7abcd7 - at /stable-dist-rustc/build/src/libstd/panicking.rs:555 - 5: 0x560ed90ee2b4 - std::panicking::begin_panic::hd6eb68e27bdf6140 - at /stable-dist-rustc/build/src/libstd/panicking.rs:517 - 6: 0x560ed90ee1d9 - std::panicking::begin_panic_fmt::abcd5965948b877f8 - at /stable-dist-rustc/build/src/libstd/panicking.rs:501 - 7: 0x560ed90ee167 - rust_begin_unwind - at /stable-dist-rustc/build/src/libstd/panicking.rs:477 - 8: 0x560ed911401d - core::panicking::panic_fmt::hc0f6d7b2c300cdd9 - at /stable-dist-rustc/build/src/libcore/panicking.rs:69 - 9: 0x560ed9113fc8 - core::panicking::panic_bounds_check::h02a4af86d01b3e96 - at /stable-dist-rustc/build/src/libcore/panicking.rs:56 - 10: 0x560ed90e71c5 - as core::ops::Index>::index::h98abcd4e2a74c41 - at /stable-dist-rustc/build/src/libcollections/vec.rs:1392 - 11: 0x560ed90e727a - panic::main::h5d6b77c20526bc35 - at /home/you/projects/panic/src/main.rs:4 - 12: 0x560ed90f5d6a - __rust_maybe_catch_panic - at /stable-dist-rustc/build/src/libpanic_unwind/lib.rs:98 - 13: 0x560ed90ee926 - std::rt::lang_start::hd7c880a37a646e81 - at /stable-dist-rustc/build/src/libstd/panicking.rs:436 - at /stable-dist-rustc/build/src/libstd/panic.rs:361 - at /stable-dist-rustc/build/src/libstd/rt.rs:57 - 14: 0x560ed90e7302 - main - 15: 0x7f0d53f16400 - __libc_start_main - 16: 0x560ed90e6659 - _start - 17: 0x0 - + 0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace + at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49 + 1: std::sys_common::backtrace::_print + at /checkout/src/libstd/sys_common/backtrace.rs:71 + 2: std::panicking::default_hook::{{closure}} + at /checkout/src/libstd/sys_common/backtrace.rs:60 + at /checkout/src/libstd/panicking.rs:381 + 3: std::panicking::default_hook + at /checkout/src/libstd/panicking.rs:397 + 4: std::panicking::rust_panic_with_hook + at /checkout/src/libstd/panicking.rs:611 + 5: std::panicking::begin_panic + at /checkout/src/libstd/panicking.rs:572 + 6: std::panicking::begin_panic_fmt + at /checkout/src/libstd/panicking.rs:522 + 7: rust_begin_unwind + at /checkout/src/libstd/panicking.rs:498 + 8: core::panicking::panic_fmt + at /checkout/src/libcore/panicking.rs:71 + 9: core::panicking::panic_bounds_check + at /checkout/src/libcore/panicking.rs:58 + 10: as core::ops::index::Index>::index + at /checkout/src/liballoc/vec.rs:1555 + 11: panic::main + at src/main.rs:4 + 12: __rust_maybe_catch_panic + at /checkout/src/libpanic_unwind/lib.rs:99 + 13: std::rt::lang_start + at /checkout/src/libstd/panicking.rs:459 + at /checkout/src/libstd/panic.rs:361 + at /checkout/src/libstd/rt.rs:61 + 14: main + 15: __libc_start_main + 16: ``` -示例 9-1:当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息 +示例 9-2:当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息 -这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:*src/main.rs* 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。 +这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,这里便是如此。 -如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。 +示例 9-2 的输出中,backtrace 的 11 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。 -本章的后面会再次回到 `panic!` 并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用 `Result` 来从错误中恢复。 +本章的后面会再次回到 `panic!` 并讲到何时应该及何时不应该使用这个方式。接下来,我们来看看如何使用 `Result` 来从错误中恢复。 diff --git a/src/ch09-02-recoverable-errors-with-result.md b/src/ch09-02-recoverable-errors-with-result.md index c477999..2bf6518 100644 --- a/src/ch09-02-recoverable-errors-with-result.md +++ b/src/ch09-02-recoverable-errors-with-result.md @@ -2,7 +2,7 @@ > [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) >
-> commit e6d6caab41471f7115a621029bd428a812c5260e +> commit a764530433720fe09ae2d97874c25341f8322573 大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作失败,这时我们可能想要创建这个文件而不是终止进程。 @@ -19,7 +19,7 @@ enum Result { `T` 和 `E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 -让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-2 所示打开一个文件: +让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-3 所示打开一个文件: 文件名: src/main.rs @@ -31,15 +31,15 @@ fn main() { } ``` -示例 9-2:打开文件 +示例 9-3:打开文件 -如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么,为此我们将 `let f` 语句改为: +如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么.让我们试试:我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下: ```rust,ignore let f: u32 = File::open("hello.txt"); ``` -现在尝试编译会给出如下错误: +现在尝试编译会给出如下输出: ```text error[E0308]: mismatched types @@ -50,16 +50,16 @@ error[E0308]: mismatched types `std::result::Result` | = note: expected type `u32` - = note: found type `std::result::Result` + found type `std::result::Result` ``` -这就告诉我们了 `File::open` 函数的返回值类型是 `Result`。这里泛型参数 `T` 放入了成功值的类型 `std::fs::File`,它是一个文件句柄。`E` 被用在失败值上时其类型是 `std::io::Error`。 +这就告诉我们了 `File::open` 函数的返回值类型是 `Result`。这里泛型参数 `T` 放入了成功值的类型 `std::fs::File`,它是一个文件句柄。`E` 被用在失败值上时 `E` 的类型是 `std::io::Error`。 这个返回值类型说明 `File::open` 调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。`File::open` 需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是 `Result` 枚举可以提供的。 -当 `File::open` 成功的情况下,变量 `f` 的值将会是一个包含文件句柄的 `Ok` 实例。在失败的情况下,`f` 会是一个包含更多关于出现了何种错误信息的 `Err` 实例。 +当 `File::open` 成功的情况下,变量 `f` 的值将会是一个包含文件句柄的 `Ok` 实例。在失败的情况下,`f` 的值会是一个包含更多关于出现了何种错误信息的 `Err` 实例。 -我们需要在示例 9-2 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-3 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。 +我们需要在示例 9-3 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-4 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。 文件名: src/main.rs @@ -78,7 +78,7 @@ fn main() { } ``` -示例 9-3:使用 `match` 表达式处理可能的 `Result` 成员 +示例 9-4:使用 `match` 表达式处理可能的 `Result` 成员 注意与 `Option` 枚举一样,`Result` 枚举和其成员也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok` 和 `Err` 之前指定 `Result::`。 @@ -88,12 +88,14 @@ fn main() { ```text thread 'main' panicked at 'There was a problem opening the file: Error { repr: -Os { code: 2, message: "No such file or directory" } }', src/main.rs:8 +Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12 ``` +输出一如既往告诉了我们到底出了什么错。 + ### 匹配不同的错误 -示例 9-3 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像示例 9-3 那样 `panic!`。让我们看看示例 9-4,其中 `match` 增加了另一个分支: +示例 9-4 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像示例 9-4 那样 `panic!`。让我们看看示例 9-5,其中 `match` 增加了另一个分支: 文件名: src/main.rs @@ -127,7 +129,7 @@ fn main() { } ``` -示例 9-4:使用不同的方式处理不同类型的错误 +示例 9-5:使用不同的方式处理不同类型的错误 `File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。 @@ -135,9 +137,9 @@ fn main() { 在 match guard 中我们想要检查的条件是 `error.kind()` 是否是 `ErrorKind` 枚举的 `NotFound` 成员。如果是,尝试用 `File::create` 创建文件。然而 `File::create` 也可能会失败,还需要增加一个内部 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 `match` 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。 -### 失败时 panic 的捷径:`unwrap` 和 `expect` +### 失败时 panic 的简写:`unwrap` 和 `expect` -`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明意图。`Result` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-3 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。 +`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。 ```rust,should_panic use std::fs::File; @@ -152,10 +154,12 @@ fn main() { ```text thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 2, message: "No such file or directory" } }', -/stable-dist-rustc/build/src/libcore/result.rs:868 +src/libcore/result.rs:906:4 ``` -还有另一个类似于 `unwrap` 的方法它还允许我们选择 `panic!` 的错误信息:`expect`。使用 `expect` 而不是 `unwrap` 并提供一个好的错误信息可以表明你的意图并有助于追踪 panic 的根源。`expect` 的语法看起来像这样: +还有另一个类似于 `unwrap` 的方法它还允许我们选择 `panic!` 的错误信息:`expect`。使用 `expect` 而不是 `unwrap` 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。`expect` 的语法看起来像这样: + +文件名: src/main.rs ```rust,should_panic use std::fs::File; @@ -169,15 +173,18 @@ fn main() { ```text thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code: -2, message: "No such file or directory" } }', -/stable-dist-rustc/build/src/libcore/result.rs:868 +2, message: "No such file or directory" } }', src/libcore/result.rs:906:4 ``` +因为这个错误信息以我们指定的文本开始,`Failed to open hello.txt`,将会更容易找到代码中的错误信息来自何处。如果在多处使用 `unwrap`,则需要花更多的时间来分析到底是哪一个 `unwrap` 造成了 panic,因为所有的 `unwrap` 调用都打印相同的信息。 + ### 传播错误 当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 **传播**(*propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。 -例如,示例 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码: +例如,示例 9-6 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码: + +Filename: src/main.rs ```rust use std::io; @@ -201,21 +208,23 @@ fn read_username_from_file() -> Result { } ``` -示例 9-5:一个函数使用 `match` 将错误返回给代码调用者 +示例 9-6:一个函数使用 `match` 将错误返回给代码调用者 首先让我们看看函数的返回值:`Result`。这意味着函数返回一个 `Result` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String` 的 `Ok` 值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。 -函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-3 中的 `match`,唯一的区别是当 `Err` 时不再调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。 +函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-4 中的 `match`,唯一的区别是当 `Err` 时不再调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。 -接着我们在变量 `s` 中创建了一个新 `String` 并调用文件句柄 `f` 的 `read_to_string` 方法来将文件的内容读取到 `s` 中。`read_to_string` 方法也返回一个 `Result` 因为它也可能会失败:哪怕是 `File::open` 已经成功了。所以我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进 `Ok` 的 `s` 中。如果`read_to_string` 失败了,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。并不需要显式的调用 `return`,因为这是函数的最后一个表达式。 +接着我们在变量 `s` 中创建了一个新 `String` 并调用文件句柄 `f` 的 `read_to_string` 方法来将文件的内容读取到 `s` 中。`read_to_string` 方法也返回一个 `Result` 因为它也可能会失败:哪怕是 `File::open` 已经成功了。所以我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进 `Ok` 的 `s` 中。如果`read_to_string` 失败了,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。不过并不需要显式的调用 `return`,因为这是函数的最后一个表达式。 调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error` 的 `Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。 这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:`?`。 -### 传播错误的捷径:`?` +### 传播错误的简写:`?` -示例 9-6 展示了一个 `read_username_from_file` 的实现,它实现了与示例 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的: +示例 9-7 展示了一个 `read_username_from_file` 的实现,它实现了与示例 9-6 中的代码相同的功能,不过这个实现使用了问号运算符: + +文件名: src/main.rs ```rust use std::io; @@ -232,11 +241,15 @@ fn read_username_from_file() -> Result { 示例 9-6:一个使用 `?` 向调用者返回错误的函数 -`Result` 值之后的 `?` 被定义为与示例 9-5 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 +`Result` 值之后的 `?` 被定义为与示例 9-6 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 -在示例 9-6 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将一些 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`。 +示例 9-6 中的 `match` 表达式与问号运算符所做的有一点不同:`?` 所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。到问号运算符调用 `from` 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 `from` 函数来定义如将其转换为返回的错误类型,问号运算符会自动处理这些转换。 -`?` 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码: +在示例 9-7 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将一些 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`。 + +`?` 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码,如示例 9-8 所示: + +文件名: src/main.rs ```rust use std::io; @@ -252,13 +265,15 @@ fn read_username_from_file() -> Result { } ``` -在 `s` 中创建新的 `String` 被放到了函数开头;这没有什么变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `s` 的 `Ok` 值。其功能再一次与示例 9-5 和示例 9-6 保持一致,不过这是一个与众不同且更符合工程学的写法。 +示例 9-8:问号运算符之后的链式方法调用 + +在 `s` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `s` 的 `Ok` 值。其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学的写法。 ### `?` 只能被用于返回 `Result` 的函数 -`?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与示例 9-5 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。 +`?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与示例 9-6 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。 -让我们看看在 `main` 函数中使用 `?` 会发生什么,如果你还记得的话它的返回值类型是`()`: +让我们看看在 `main` 函数中使用 `?` 会发生什么,如果你还记得的话其返回值类型是`()`: ```rust,ignore use std::fs::File; @@ -268,29 +283,23 @@ fn main() { } ``` - - 当编译这些代码,会得到如下错误信息: ```text -error[E0308]: mismatched types - --> +error[E0277]: the trait bound `(): std::ops::Try` is not satisfied + --> src/main.rs:4:13 | -3 | let f = File::open("hello.txt")?; - | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected (), found enum -`std::result::Result` +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 | - = note: expected type `()` - = note: found type `std::result::Result<_, _>` + = help: the trait `std::ops::Try` is not implemented for `()` + = note: required by `std::ops::Try::from_error` ``` -错误指出存在不匹配的类型:`main` 函数返回一个 `()` 类型,而 `?` 返回一个 `Result`。编写不返回 `Result` 的函数时,如果调用其他返回 `Result` 的函数,需要使用 `match` 或者 `Result` 的方法之一来处理它,而不能用 `?` 将潜在的错误传播给调用者。 +错误指出只能在返回 `Result` 的函数中使用问号运算符。在不返回 `Result` 的函数中,当调用其他返回 `Result` 的函数时,需要使用 `match` 或 `Result` 的方法之一来处理,而不能用 `?` 将潜在的错误传播给调用者。 现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候返回他们各自适合哪些场景的话题了。 diff --git a/src/ch09-03-to-panic-or-not-to-panic.md b/src/ch09-03-to-panic-or-not-to-panic.md index 280f5c6..93abbbc 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -2,7 +2,7 @@ > [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) >
-> commit 88dee413c0792dcaf563d753e00400cc191f1fbe +> commit 3e79fb6f3f85ac6d4a0ce46612e5a7381dc7f1b1 那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。