From 31626f6ee80d093b1d3c0482029d7fd44772668f Mon Sep 17 00:00:00 2001 From: kazeno Date: Thu, 22 May 2025 14:46:33 +0800 Subject: [PATCH] wip: 2024 edition --- src/ch08-03-hash-maps.md | 24 +++--- src/ch09-00-error-handling.md | 9 +-- ...ch09-01-unrecoverable-errors-with-panic.md | 48 ++++++------ src/ch09-02-recoverable-errors-with-result.md | 76 +++++++++---------- src/ch09-03-to-panic-or-not-to-panic.md | 25 +++--- ...improving-error-handling-and-modularity.md | 2 +- 6 files changed, 92 insertions(+), 92 deletions(-) diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index bb15d4c..1e2d7c6 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -66,7 +66,7 @@ Blue: 10 尽管键值对的数量是可以增长的,每个唯一的键只能同时关联一个值(反之不一定成立:比如蓝队和黄队的 `scores` 哈希 map 中都可能存储有 10 这个值)。 -当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 **没有** 对应值时增加新值。或者可以结合新旧两值。让我们看看这分别该如何处理! +当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键**没有**对应值时增加新值。或者可以结合新旧两值。让我们看看这分别该如何实现! #### 覆盖一个值 @@ -80,11 +80,11 @@ Blue: 10 这会打印出 `{"Blue": 25}`。原始的值 `10` 则被覆盖了。 -#### 只在键没有对应值时插入键值对 +#### 只在键尚不存在时插入键值对 -我们经常会检查某个特定的键是否已经存在于哈希 map 中并进行如下操作:如果哈希 map 中键已经存在则不做任何操作。如果不存在则连同值一块插入。 +我们经常会检查某个特定的键是否已经存在于哈希 map 中并进行如下操作:如果哈希 map 中键已经存在则不做任何操作;如果不存在则连同值一块插入。 -为此哈希 map 有一个特有的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 `entry` API 的代码看起来像示例 8-24 这样: +为此哈希 map 有一个专用的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举 `Entry` 它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 `50`,对于蓝队也是如此。使用 `entry` API 的代码看起来如示例 8-24 所示。 ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-24/src/main.rs:here}} @@ -98,7 +98,7 @@ Blue: 10 #### 根据旧值更新一个值 -另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,示例 8-25 中的代码计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 `0`。 +另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,示例 8-25 中的代码计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就先插入值 `0`。 ```rust {{#rustdoc_include ../listings/ch08-common-collections/listing-08-25/src/main.rs:here}} @@ -106,13 +106,13 @@ Blue: 10 示例 8-25:通过哈希 map 储存单词和计数来统计出现次数 -这会打印出 `{"world": 2, "hello": 1, "wonderful": 1}`。你可能会看到相同的键值对以不同的顺序打印:回忆一下[“访问哈希 map 中的值”][access]部分中遍历哈希 map 会以任意顺序进行。 +这会打印出 `{"world": 2, "hello": 1, "wonderful": 1}`。你可能会看到相同的键值对以不同的顺序打印:回忆一下[“访问哈希 map 中的值”][access]部分中提到遍历哈希 map 会以任意顺序进行。 `split_whitespace` 方法返回一个由空格分隔 `text` 值子 slice 的迭代器。`or_insert` 方法返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在 `count` 变量中,所以为了赋值必须首先使用星号(`*`)解引用 `count`。这个可变引用在 `for` 循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。 ### 哈希函数 -`HashMap` 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)[^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 的类型。[第十章][traits]会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 [^siphash]: [https://en.wikipedia.org/wiki/SipHash](https://en.wikipedia.org/wiki/SipHash) @@ -120,15 +120,15 @@ Blue: 10 vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题: -* 给定一系列数字,使用 vector 并返回这个列表的中位数(排列数组后位于中间的值)和众数(出现次数最多的值;在这里哈希 map 会很有帮助)。 -* 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 “ay”,所以 “first” 会变成 “irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple” 会变成 “apple-hay”)。牢记 UTF-8 编码! -* 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字典序排列的列表。 +1. 给定一组整数,使用 vector 并返回这个列表的中位数(排列数组后位于中间的值)和众数(出现次数最多的值;在这里哈希 map 会很有帮助)。 +2. 将字符串转换为 pig latin。也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 *ay*,所以 *first* 会变成 *irst-fay*。元音字母开头的单词则在结尾增加 *hay*(*apple* 会变成 *apple-hay*)。请注意 UTF-8 编码的细节! +3. 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字典序排列的列表。 标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习! -我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机! +我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!接下来我们将讨论这一部分! -[iterators]: ch13-02-iterators.html [validating-references-with-lifetimes]: ch10-03-lifetime-syntax.html#生命周期确保引用有效 [access]: #访问哈希-map-中的值 +[traits]: ch10-02-traits.html diff --git a/src/ch09-00-error-handling.md b/src/ch09-00-error-handling.md index 81412a0..97cbc2d 100644 --- a/src/ch09-00-error-handling.md +++ b/src/ch09-00-error-handling.md @@ -1,11 +1,10 @@ # 错误处理 -> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/main/src/ch09-00-error-handling.md) ->
-> commit 199ca99926f232ee7f581a917eada4b65ff21754 + + -错误是软件中不可否认的事实,所以 Rust 有一些处理出错情况的特性。在许多情况下,Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产环境之前就能发现错误并进行适当的处理。 +错误是软件开发中不可避免的事实,所以 Rust 有一些处理出错情况的特性。在许多情况下,Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产环境之前就能发现错误并进行适当的处理。 Rust 将错误分为两大类:**可恢复的**(*recoverable*)和 **不可恢复的**(*unrecoverable*)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。 -大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理它们。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 1e9351f..7e4673b 100644 --- a/src/ch09-01-unrecoverable-errors-with-panic.md +++ b/src/ch09-01-unrecoverable-errors-with-panic.md @@ -1,16 +1,15 @@ ## 用 `panic!` 处理不可恢复的错误 -> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/main/src/ch09-01-unrecoverable-errors-with-panic.md) ->
-> commit 2921743516b3e2c0f45a95390e7b536e42f4af7c + + 突然有一天,代码出问题了,而你对此束手无策。对于这种情况,Rust 有 `panic!`宏。在实践中有两种方法造成 panic:执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者显式调用 `panic!` 宏。这两种情况都会使程序 panic。通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。通过一个环境变量,你也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。 -> ### 对应 panic 时的栈展开或终止 +> ### 响应 panic 时的栈展开或终止 > > 当出现 panic 时,程序默认会开始 **展开**(*unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 **终止**(*abort*),这会不清理数据就退出程序。 > -> 那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 *Cargo.toml* 的 `[profile]` 部分增加 `panic = 'abort'`,可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止: +> 那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 *Cargo.toml* 的 `[profile]` 部分增加 `panic = 'abort'`,可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止,可添加: > > ```toml > [profile.release] @@ -33,11 +32,12 @@ 最后两行包含 `panic!` 调用造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:5* 表明这是 *src/main.rs* 文件的第二行第五个字符。 -在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 `panic!` 宏的调用。在其他情况下,`panic!` 可能会出现在我们的代码所调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。我们可以使用 `panic!` 被调用的函数的 backtrace 来寻找代码中出问题的地方。下面我们会详细介绍 backtrace 是什么。 +在这个例子中,被指明的那一行是我们代码的一部分,如果跳转到该行,就会发现 `panic!` 宏的调用。在其它情况下,`panic!` 可能会出现在我们的代码所调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。 ### 使用 `panic!` 的 backtrace -让我们来看看另一个因为我们代码中的 bug 引起的别的库中 `panic!` 的例子,而不是直接的宏调用。示例 9-1 有一些尝试通过索引访问 vector 中元素的例子: + +我们可以使用 `panic!` 被调用的函数的 backtrace 来寻找代码中出问题的地方。下面我们会详细介绍 backtrace 是什么。为了了解如何使用 `panic!` 的 backtrace,让我们来看另一个示例,我们代码中的 bug 引起的别的库中 `panic!` 的例子,而不是直接的宏调用看起来如何。示例 9-1 有一些尝试通过索引访问 vector 中超出有效范围元素的例子: 文件名:src/main.rs @@ -47,46 +47,50 @@ 示例 9-1:尝试访问超越 vector 结尾的元素,这会造成 `panic!` -这里尝试访问 vector 的第一百个元素(这里的索引是 99 因为索引从 0 开始),不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。 +这里尝试访问 vector 的第 100 个元素(这里的索引是 99 因为索引从 0 开始),不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确元素。 -C 语言中,尝试读取数据结构之后的值是未定义行为(undefined behavior)。你会得到任何对应数据结构中这个元素的内存位置的值,甚至是这些内存并不属于这个数据结构的情况。这被称为 **缓冲区溢出**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数据结构之后不被允许的数据。 +C 语言中,尝试读取数据结构之后的值是未定义行为(undefined behavior)。你会得到任何对应数据结构中这个元素的内存位置的值,甚至是这些内存并不属于这个数据结构的情况。这被称为 **缓存区过读**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数据结构之后未经授权的数据。 -为了保护程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下输出: +为了保护程序不受此类漏洞的影响,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。让我们来试一试,看看结果: ```console {{#include ../listings/ch09-error-handling/listing-09-01/output.txt}} ``` -错误指向 `main.rs` 的第 4 行,这里我们尝试访问索引 99。下面的说明(note)行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace。*backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 `RUST_BACKTRACE` 环境变量设置为任何不是 0 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出: +错误指向 *main.rs* 的第 4 行,这里我们试图访问向量 `v` 中的索引 `99`。 + + +`note:` 告诉我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace。*backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 `RUST_BACKTRACE` 环境变量设置为任何不是 `0` 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出: ```console $ RUST_BACKTRACE=1 cargo run -thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 +thread 'main' panicked at src/main.rs:4:6: +index out of bounds: the len is 3 but the index is 99 stack backtrace: 0: rust_begin_unwind - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5 + at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5 1: core::panicking::panic_fmt - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14 + at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14 2: core::panicking::panic_bounds_check - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5 + at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5 3: >::index - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10 + at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10 4: core::slice::index:: for [T]>::index - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9 + at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9 5: as core::ops::index::Index>::index - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9 + at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9 6: panic::main - at ./src/main.rs:4:5 + at ./src/main.rs:4:6 7: core::ops::function::FnOnce::call_once - at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5 + at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. ``` 示例 9-2:当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息 -这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 `--release` 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样。 +这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用调试符号(debug symbols)。当不使用 `--release` 参数运行 cargo build 或 cargo run 时调试符号会默认启用,就像这里一样。 -示例 9-2 的输出中,backtrace 的 12 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在示例 9-1 中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。 +示例 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` 来从错误中恢复。 diff --git a/src/ch09-02-recoverable-errors-with-result.md b/src/ch09-02-recoverable-errors-with-result.md index 00dc0cf..d232cbc 100644 --- a/src/ch09-02-recoverable-errors-with-result.md +++ b/src/ch09-02-recoverable-errors-with-result.md @@ -1,12 +1,11 @@ ## 用 `Result` 处理可恢复的错误 -> [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/main/src/ch09-02-recoverable-errors-with-result.md) ->
-> commit 699adc6f5cb76f6e9d567ff0a57d8a844ac07a88 + + -大部分错误并没有严重到需要程序完全停止执行。有时候,一个函数失败,仅仅就是因为一个容易理解和响应的原因。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。 +大部分错误并没有严重到需要程序完全停止执行。有时函数失败的原因很容易理解并加以处理。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。 -回忆一下第二章 [“使用 `Result` 类型来处理潜在的错误”][handle_failure] 部分中的那个 `Result` 枚举,它定义有如下两个成员,`Ok` 和 `Err`: +回忆一下第二章 [“使用 `Result` 类型来处理潜在的错误”][handle_failure] 部分中的那个 `Result` 枚举,它定义有如下两个变体,`Ok` 和 `Err`: ```rust enum Result { @@ -15,9 +14,9 @@ enum Result { } ``` -`T` 和 `E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 +`T` 和 `E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 变体中的数据的类型,而 `E` 代表失败时返回的 `Err` 变体中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 -让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-3 所示打开一个文件: +让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-3 所示尝试打开一个文件: 文件名:src/main.rs @@ -31,7 +30,7 @@ enum Result { 当 `File::open` 成功时,`greeting_file_result` 变量将会是一个包含文件句柄的 `Ok` 实例。当失败时,`greeting_file_result` 变量将会是一个包含了更多关于发生了何种错误的信息的 `Err` 实例。 -我们需要在示例 9-3 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-4 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。 +我们需要在示例 9-3 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-4 展示了一个使用基本工具处理 `Result` 的例子,第六章学习过的 `match` 表达式。 文件名:src/main.rs @@ -39,11 +38,11 @@ enum Result { {{#rustdoc_include ../listings/ch09-error-handling/listing-09-04/src/main.rs}} ``` -示例 9-4:使用 `match` 表达式处理可能会返回的 `Result` 成员 +示例 9-4:使用 `match` 表达式处理可能会返回的 `Result` 变体 -注意与 `Option` 枚举一样,`Result` 枚举和其成员也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok` 和 `Err` 之前指定 `Result::`。 +注意与 `Option` 枚举一样,`Result` 枚举和其变体也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok` 和 `Err` 之前指定 `Result::`。 -这里我们告诉 Rust 当结果是 `Ok` 时,返回 `Ok` 成员中的 `file` 值,然后将这个文件句柄赋值给变量 `greeting_file`。`match` 之后,我们可以利用这个文件句柄来进行读写。 +这里我们告诉 Rust 当结果是 `Ok` 时,返回 `Ok` 变体中的 `file` 值,然后将这个文件句柄赋值给变量 `greeting_file`。`match` 之后,我们可以利用这个文件句柄来进行读写。 `match` 的另一个分支处理从 `File::open` 得到 `Err` 值的情况。在这种情况下,我们选择调用 `panic!` 宏。如果当前目录没有一个叫做 *hello.txt* 的文件,当运行这段代码时会看到如下来自 `panic!` 宏的输出: @@ -55,7 +54,7 @@ enum Result { ### 匹配不同的错误 -示例 9-4 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像示例 9-4 那样 `panic!`。让我们看看示例 9-5,其中 `match` 增加了另一个分支: +示例 9-4 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败 -- 例如没有打开文件的权限 -- 我们仍然希望像示例 9-4 那样 `panic!`。为此,我们在示例 9-5 中添加了一个内部 `match` 表达式,如下所示: 文件名:src/main.rs @@ -65,13 +64,13 @@ enum Result { 示例 9-5:使用不同的方式处理不同类型的错误 -`File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。这样,`match` 就匹配完 `greeting_file_result` 了,不过对于 `error.kind()` 还有一个内层 `match`。 +`File::open` 返回的 `Err` 变体中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的变体对应 `io` 操作可能导致的不同错误类型。我们感兴趣的变体是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。这样,`match` 就匹配完 `greeting_file_result` 了,不过对于 `error.kind()` 还有一个内层 `match`。 -我们希望在内层 `match` 中检查的条件是 `error.kind()` 的返回值是否为 `ErrorKind`的 `NotFound` 成员。如果是,则尝试通过 `File::create` 创建文件。然而因为 `File::create` 也可能会失败,还需要增加一个内层 `match` 语句。当文件不能被创建,会打印出一个不同的错误信息。外层 `match` 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。 +我们希望在内层 `match` 中检查的条件是 `error.kind()` 的返回值是否为 `ErrorKind`的 `NotFound` 变体。如果是,则通过 `File::create` 尝试创建该文件。然而因为 `File::create` 也可能会失败,还需要在内层 `match` 表达式中增加了第二个分支。当文件不能被创建,会打印出一个不同的错误信息。外层 `match` 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。 -> 不同于使用 `match` 和 `Result` +> #### 使用 `match` 处理 `Result` 的替代方案 > -> 这里有好多 `match`!`match` 确实很强大,不过也非常的原始。第十三章我们会介绍闭包(closure),它会和定义在 `Result` 中的很多方法一起使用。在处理代码中的 `Result` 值时,相比于使用 `match` ,使用这些方法会更加简洁。 +> 这里有好多 `match`!`match` 确实很强大,不过也非常的原始。第十三章我们会介绍闭包(closure),它会和定义在 `Result` 中的很多方法一起使用。在处理代码中的 `Result` 值时,使用这些方法往往比直接写 `match` 更简洁。 > > 例如,这是另一个编写与示例 9-5 逻辑相同但是使用闭包和 `unwrap_or_else` 方法的例子: > @@ -83,10 +82,10 @@ enum Result { > let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { > if error.kind() == ErrorKind::NotFound { > File::create("hello.txt").unwrap_or_else(|error| { -> panic!("Problem creating the file: {:?}", error); +> panic!("Problem creating the file: {error:?}"); > }) > } else { -> panic!("Problem opening the file: {:?}", error); +> panic!("Problem opening the file: {error:?}"); > } > }); > } @@ -94,9 +93,9 @@ enum Result { > > 虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 `match` 表达式且更容易阅读。在阅读完第十三章后再回到这个例子,并查看标准库文档 `unwrap_or_else` 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 `match` 表达式。 -### 失败时 panic 的简写:`unwrap` 和 `expect` +### 失败时 panic 的快捷方式:`unwrap` 和 `expect` -`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。这里是一个实践 `unwrap` 的例子: +`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result` 类型定义了很多辅助方法来处理各种更为特定的任务。`unwrap` 方法是一个快捷方式,其内部实现与我们在 Listing 9-4 中编写的 `match` 表达式相同。如果 `Result` 值是变体 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是变体 `Err`,`unwrap` 会为我们调用 `panic!`。这里是一个实践 `unwrap` 的例子: 文件名:src/main.rs @@ -112,7 +111,7 @@ code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49 ``` -还有另一个类似于 `unwrap` 的方法它还允许我们选择 `panic!` 的错误信息:`expect`。使用 `expect` 而不是 `unwrap` 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。`expect` 的语法看起来像这样: +同样,`expect` 方法也允许我们自定义 `panic!` 的错误信息。使用 `expect` 而不是 `unwrap` 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。`expect` 的语法看起来像这样: 文件名:src/main.rs @@ -123,16 +122,15 @@ src/main.rs:4:49 `expect` 与 `unwrap` 的使用方式一样:返回文件句柄或调用 `panic!` 宏。`expect` 在调用 `panic!` 时使用的错误信息将是我们传递给 `expect` 的参数,而不像 `unwrap` 那样使用默认的 `panic!` 信息。它看起来像这样: ```text -thread 'main' panicked at 'hello.txt should be included in this project: Error -{ repr: Os { code: 2, message: "No such file or directory" } }', -src/libcore/result.rs:906:4 +thread 'main' panicked at src/main.rs:5:10: +hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" } ``` 在生产级别的代码中,大部分 Rustaceans 选择 `expect` 而不是 `unwrap` 并提供更多关于为何操作期望是一直成功的上下文。如此如果该假设真的被证明是错的,你也有更多的信息来用于调试。 ### 传播错误 -当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 **传播**(*propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。 +当函数的实现中调用了可能会失败的操作时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为**传播**(*propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。 例如,示例 9-6 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码: @@ -144,19 +142,19 @@ src/libcore/result.rs:906:4 示例 9-6:一个函数使用 `match` 将错误返回给代码调用者 -这个函数可以编写成更加简短的形式,不过我们以大量手动处理开始以便探索错误处理;在最后我们会展示更短的形式。让我们看看函数的返回值:`Result`。这意味着函数返回一个 `Result` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。 +这个函数可以编写成更加简短的形式,不过我们以大量手动处理开始以便探索错误处理;在最后我们会展示更简洁的形式。让我们看看函数的返回值:`Result`。这意味着函数返回一个 `Result` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。 如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String` 的 `Ok` 值 —— 函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。 函数体以调用 `File::open` 函数开始。接着使用 `match` 处理返回值 `Result`,类似示例 9-4,如果 `File::open` 成功了,模式变量 `file` 中的文件句柄就变成了可变变量 `username_file` 中的值,接着函数继续执行。在 `Err` 的情况下,我们没有调用 `panic!`,而是使用 `return` 关键字提前结束整个函数,并将来自 `File::open` 的错误值(现在在模式变量 `e` 中)作为函数的错误值传回给调用者。 -所以,如果在 `username_file` 中有一个文件句柄,该函数随后会在变量 `username` 中创建一个新的 `String` 并调用文件句柄 `username_file` 上的 `read_to_string` 方法,以将文件的内容读入 `username`。`read_to_string` 方法也返回一个 `Result`,因为它可能会失败,哪怕是 `File::open` 已经成功了。因此,我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 执行成功,那么这个函数也就成功了,我们将从文件中读取的用户名返回,此时用户名位于被封装进 `Ok` 的 `username` 中。如果 `read_to_string` 执行失败,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。然而,我们无需显式调用 `return` 语句,因为这是函数的最后一个表达式。 +所以,如果在 `username_file` 中有一个文件句柄,该函数随后会在变量 `username` 中创建一个新的 `String` 并调用文件句柄 `username_file` 上的 `read_to_string` 方法,以将文件的内容读入 `username`。`read_to_string` 方法也返回一个 `Result`,因为它可能会失败,哪怕是 `File::open` 已经成功了。因此,我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 执行成功,那么这个函数也就成功了,我们将从文件中读取的用户名返回,此时用户名位于被封装进 `Ok` 的 `username` 中。如果 `read_to_string` 执行失败,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。然而,我们无需显式写出 `return`,因为这是函数的最后一个表达式。 调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error` 的 `Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。 -这种传播错误的模式在 Rust 是如此的常见,以至于 Rust 提供了 `?` 问号运算符来使其更易于处理。 +这种传播错误的模式在 Rust 是如此的常见,以至于 Rust 提供了 `?` 问号运算符来来简化这一过程。 -### 传播错误的简写:`?` 运算符 +### 传播错误的快捷方式:`?` 运算符 示例 9-7 展示了一个 `read_username_from_file` 的实现,它实现了与示例 9-6 中的代码相同的功能,不过这个实现使用了 `?` 运算符: @@ -168,7 +166,7 @@ src/libcore/result.rs:906:4 示例 9-7:一个使用 `?` 运算符向调用者返回错误的函数 -`Result` 值之后的 `?` 被定义为与示例 9-6 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 +`Result` 值之后的 `?` 被定义为与示例 9-6 中定义的处理 `Result` 值的 `match` 表达式有着几乎完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 示例 9-6 中的 `match` 表达式与 `?` 运算符所做的有一点不同:`?` 运算符所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。当 `?` 运算符调用 `from` 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。 @@ -176,7 +174,7 @@ src/libcore/result.rs:906:4 在示例 9-7 的上下文中,`File::open` 调用结尾的 `?` 会将 `Ok` 中的值返回给变量 `username_file`。如果发生了错误,`?` 运算符会使整个函数提前返回并将任何 `Err` 值返回给调用代码。同理也适用于 `read_to_string` 调用结尾的 `?`。 -`?` 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码,如示例 9-8 所示: +`?` 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步简化代码,如示例 9-8 所示: 文件名:src/main.rs @@ -186,7 +184,7 @@ src/libcore/result.rs:906:4 示例 9-8:问号运算符之后的链式方法调用 -在 `username` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `username_file`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `username` 的 `Ok` 值。其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学(ergonomic)的写法。 +在 `username` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `username_file`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `username` 的 `Ok` 值,而不是返回错误。其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学(ergonomic)的写法。 示例 9-9 展示了一个使用 `fs::read_to_string` 的更为简短的写法: @@ -198,7 +196,7 @@ src/libcore/result.rs:906:4 示例 9-9: 使用 `fs::read_to_string` 而不是打开后读取文件 -将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 `fs::read_to_string` 的函数,它会打开文件、新建一个 `String`、读取文件的内容,并将内容放入 `String`,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。 +将文件读取到一个字符串是相当常见的操作,所以标准库提供了名为 `fs::read_to_string` 的函数,它会打开文件、新建一个 `String`、读取文件的内容,并将内容放入 `String`,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。 ### 哪里可以使用 `?` 运算符 @@ -220,9 +218,9 @@ src/libcore/result.rs:906:4 {{#include ../listings/ch09-error-handling/listing-09-10/output.txt}} ``` -这个错误指出只能在返回 `Result` 或者其它实现了 `FromResidual` 的类型的函数中使用 `?` 运算符。 +这个错误指出只能在返回 `Result`、`Option` 或者其它实现了 `FromResidual` 的类型的函数中使用 `?` 运算符。 -为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为 `Result`。另一个是使用 `match` 或 `Result` 的方法中合适的一个来处理 `Result`。 +为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为改为与你在 `?` 运算符所作用的值兼容的类型。另一个是使用 `match` 或者 `Result` 类型的方法,以适当的方式处理 `Result`。 错误信息也提到 `?` 也可用于 `Option` 值。如同对 `Result` 使用 `?` 一样,只能在返回 `Option` 的函数中对 `Option` 使用 `?`。在 `Option` 上调用 `?` 运算符的行为与 `Result` 类似:如果值是 `None`,此时 `None` 会从函数中提前返回。如果值是 `Some`,`Some` 中的值作为表达式的返回值同时函数继续。示例 9-11 中有一个从给定文本中返回第一行最后一个字符的函数的例子: @@ -234,13 +232,13 @@ src/libcore/result.rs:906:4 这个函数返回 `Option` 因为它可能会在这个位置找到一个字符,也可能没有字符。这段代码获取 `text` 字符串 slice 作为参数并调用其 `lines` 方法,这会返回一个字符串中每一行的迭代器。因为函数希望检查第一行,所以调用了迭代器 `next` 来获取迭代器中第一个值。如果 `text` 是空字符串,`next` 调用会返回 `None`,此时我们可以使用 `?` 来停止并从 `last_char_of_first_line` 返回 `None`。如果 `text` 不是空字符串,`next` 会返回一个包含 `text` 中第一行的字符串 slice 的 `Some` 值。 -`?` 会提取这个字符串 slice,然后可以在字符串 slice 上调用 `chars` 来获取字符的迭代器。我们感兴趣的是第一行的最后一个字符,所以可以调用 `last` 来返回迭代器的最后一项。这是一个 `Option`,因为有可能第一行是一个空字符串,例如 `text` 以一个空行开头而后面的行有文本,像是 `"\nhi"`。不过,如果第一行有最后一个字符,它会返回在一个 `Some` 成员中。`?` 运算符作用于其中给了我们一个简洁的表达这种逻辑的方式。如果我们不能在 `Option` 上使用 `?` 运算符,则不得不使用更多的方法调用或者 `match` 表达式来实现这些逻辑。 +`?` 会提取这个字符串 slice,然后可以在字符串 slice 上调用 `chars` 来获取字符的迭代器。我们感兴趣的是第一行的最后一个字符,所以可以调用 `last` 来返回迭代器的最后一项。这是一个 `Option`,因为有可能第一行是一个空字符串;例如 `text` 以一个空行开头而后面的行有文本,像是 `"\nhi"`。不过,如果第一行有最后一个字符,它会返回在一个 `Some` 变体中。`?` 运算符作用于其中给了我们一个简洁的表达这种逻辑的方式。如果我们不能在 `Option` 上使用 `?` 运算符,则不得不使用更多的方法调用或者 `match` 表达式来实现这些逻辑。 注意你可以在返回 `Result` 的函数中对 `Result` 使用 `?` 运算符,可以在返回 `Option` 的函数中对 `Option` 使用 `?` 运算符,但是不可以混合搭配。`?` 运算符不会自动将 `Result` 转化为 `Option`,反之亦然;在这些情况下,可以使用类似 `Result` 的 `ok` 方法或者 `Option` 的 `ok_or` 方法来显式转换。 目前为止,我们所使用的所有 `main` 函数都返回 `()`。`main` 函数是特殊的因为它是可执行程序的入口点和退出点,为了使程序能正常工作,其可以返回的类型是有限制的。 -幸运的是 `main` 函数也可以返回 `Result<(), E>`,示例 9-12 中的代码来自示例 9-10 不过修改了 `main` 的返回值为 `Result<(), Box>` 并在结尾增加了一个 `Ok(())` 作为返回值。这段代码可以编译: +幸运的是 `main` 函数也可以返回 `Result<(), E>`,示例 9-12 中的代码来自示例 9-10 不过修改了 `main` 的返回值为 `Result<(), Box>` 并在结尾增加了一个 `Ok(())` 作为返回值。这段代码就可以编译了。 ```rust,ignore {{#rustdoc_include ../listings/ch09-error-handling/listing-09-12/src/main.rs}} @@ -248,13 +246,13 @@ src/libcore/result.rs:906:4 示例 9-12: 修改 `main` 返回 `Result<(), E>` 允许对 `Result` 值使用 `?` 运算符 -`Box` 类型是一个 **trait 对象**(*trait object*)第十八章 [顾及不同类型值的 trait 对象”][trait-objects] 部分会做介绍。目前可以将 `Box` 理解为 “任何类型的错误”。在返回 `Box` 错误类型 `main` 函数中对 `Result` 使用 `?` 是允许的,因为它允许任何 `Err` 值提前返回。即便 `main` 函数体从来只会返回 `std::io::Error` 错误类型,通过指定 `Box`,这个签名也仍是正确的,甚至当 `main` 函数体中增加更多返回其他错误类型的代码时也是如此。 +`Box` 类型是一个**trait 对象**(*trait object*)第十八章[顾及不同类型值的 trait 对象”][trait-objects] 部分会做介绍。目前可以将 `Box` 理解为 “任何类型的错误”。在返回 `Box` 错误类型 `main` 函数中对 `Result` 使用 `?` 是允许的,因为它允许任何 `Err` 值提前返回。即便 `main` 函数体从来只会返回 `std::io::Error` 错误类型,通过指定 `Box`,这个签名也仍是正确的,甚至当 `main` 函数体中增加更多返回其他错误类型的代码,这个函数签名依然保持正确。 当 `main` 函数返回 `Result<(), E>`,如果 `main` 返回 `Ok(())` 可执行程序会以 `0` 值退出,而如果 `main` 返回 `Err` 值则会以非零值退出;成功退出的程序会返回整数 `0`,运行错误的程序会返回非 `0` 的整数。Rust 也会从二进制程序中返回与这个惯例相兼容的整数。 `main` 函数也可以返回任何实现了 [`std::process::Termination` trait][termination] 的类型,它包含了一个返回 `ExitCode` 的 `report` 函数。请查阅标准库文档了解更多为自定义类型实现 `Termination` trait 的细节。 -现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到它们各自适合哪些场景的话题了。 +现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,让我们回到在不同场景下如何决定使用哪种方式的问题。 [handle_failure]: ch02-00-guessing-game-tutorial.html#使用-result-类型来处理潜在的错误 [trait-objects]: ch18-02-trait-objects.html#顾及不同类型值的-trait-对象 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 0842c8a..3439cdf 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -1,14 +1,13 @@ ## 要不要 `panic!` -> [ch09-03-to-panic-or-not-to-panic.md](https://github.com/rust-lang/book/blob/main/src/ch09-03-to-panic-or-not-to-panic.md) ->
-> commit dd8f47a74b67178cea8c832e3b4eaf3bb515bd72 + + 那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。 -在一些类似示例、原型代码(prototype code)和测试中,panic 比返回 `Result` 更为合适,下文中会讨论合适的原因,紧接着讨论另外一种特殊情况,即有些场景编译器无法认识这个分支代码是不可能走到的,但是程序员可以判断出来的,这种场景也可以用 panic!。另外章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。 +在一些类似示例、原型代码(prototype code)和测试中,panic 比返回 `Result` 更为合适,下文中会讨论合适的原因,紧接着讨论另外一种特殊情况,即有些场景编译器无法认识这个分支代码是不可能走到的,但是人类可以判断出来的,这种场景也可以用 panic!。另外章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。 -### 示例、代码原型和测试都非常适合 panic +### 示例、代码原型和测试 当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似 `unwrap` 这样可能 `panic!` 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。 @@ -18,27 +17,27 @@ ### 当我们比编译器知道更多的情况 -当你有一些其他的逻辑来确保 `Result` 会是 `Ok` 值时,调用 `unwrap` 或者 `expect` 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 `Result` 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 `Err` 值,那么调用 `unwrap` 也是完全可以接受的,这里是一个例子: +当你有一些其他的逻辑来确保 `Result` 会是 `Ok` 值时,调用 `unwrap` 或者 `expect` 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 `Result` 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 `Err` 值,那么调用 `unwrap` 也是完全可以接受的,更好的做法是在 `expect` 的提示文本中说明你认为永远不会出现 `Err` 的原因。下面是一个示例:。这里是一个例子: ```rust {{#rustdoc_include ../listings/ch09-error-handling/no-listing-08-unwrap-that-cant-fail/src/main.rs:here}} ``` -我们通过解析一个硬编码的字符来创建一个 `IpAddr` 实例。可以看出 `127.0.0.1` 是一个有效的 IP 地址,所以这里使用 `expect` 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 `parse` 方法的返回值类型:它仍然是一个 `Result` 值,而编译器仍然会要求我们处理这个 `Result`,好像还是有可能出现 `Err` 成员那样。这是因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 **确实** 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 `Result` 了。提及这个 IP 地址是硬编码的假设会促使我们将来把 `expect` 替换为更好的错误处理,我们应该从其它代码获取 IP 地址。 +我们通过解析一个硬编码的字符来创建一个 `IpAddr` 实例。可以看出 `127.0.0.1` 是一个有效的 IP 地址,所以这里使用 `expect` 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 `parse` 方法的返回值类型:它仍然是一个 `Result` 值,而编译器仍然会要求我们处理这个 `Result`,好像还是有可能出现 `Err` 变体那样。这是因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就**确实**有失败的可能性,这时确实需要我们以一种更健壮的方式处理 `Result`。提及这个 IP 地址是硬编码的假设会促使我们将来把 `expect` 替换为更好的错误处理,我们应该从其它代码获取 IP 地址。 ### 错误处理指导原则 -在当有可能会导致有害状态的情况下建议使用 `panic!` —— 在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值 —— 外加如下几种情况: +在当有可能会导致有害状态(bad state)的情况下建议使用 `panic!` —— 在这里,**有害状态**(*bad state*)是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值 —— 外加如下几种情况: -* 有害状态是非预期的行为,与偶尔会发生的行为相对,比如用户输入了错误格式的数据。 -* 在此之后代码的运行依赖于不处于这种有害状态,而不是在每一步都检查是否有问题。 -* 没有可行的手段来将有害状态信息编码进所使用的类型中的情况。我们会在第十八章 [“将状态和行为编码为类型”][encoding] 部分通过一个例子来说明我们的意思。 +- 有害状态是非预期的行为,与偶尔会发生的行为相对,比如用户输入了错误格式的数据。 +- 在此之后代码的运行依赖于不处于这种有害状态,而不是在每一步都检查是否有问题。 +- 没有可行的手段来将有害状态信息编码进所使用的类型中的情况。我们会在第十八章[“将状态和行为编码为类型”][encoding]部分通过一个例子来说明我们的意思。 如果别人调用你的代码并传递了一个没有意义的值,尽最大可能返回一个错误,如此库的用户就可以决定在这种情况下该如何处理。然而在继续执行代码是不安全或有害的情况下,最好的选择可能是调用 `panic!` 并警告库的用户他们的代码中有 bug,这样他们就会在开发时进行修复。类似的,如果你正在调用不受你控制的外部代码,并且它返回了一个你无法修复的无效状态,那么 `panic!` 往往是合适的。 -然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到格式错误的数据,或者 HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。 +然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到格式错误的数据,或者 HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,而调用者就必须决定该如何处理这个问题。 -当你的代码在进行一个使用无效值进行调用时可能将用户置于风险中的操作时,代码应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):它们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的 **程序员** 修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。 +当你的代码在进行一个使用无效值进行调用时可能将用户置于风险中的操作时,代码应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循**契约**(*contracts*):它们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的**开发者**修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中进行说明。 虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个并不是 `Option` 的类型,则程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some` 和 `None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。 diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index 2265f64..1b0aa25 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -270,4 +270,4 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在 [ch9-error-guidelines]: ch09-03-to-panic-or-not-to-panic.html#错误处理指导原则 [ch9-result]: ch09-02-recoverable-errors-with-result.html [ch18]: ch18-00-oop.html -[ch9-question-mark]: ch09-02-recoverable-errors-with-result.html#传播错误的简写-运算符 +[ch9-question-mark]: ch09-02-recoverable-errors-with-result.html#传播错误的快捷方式-运算符