update to ch09-03

pull/585/head
KaiserY 3 years ago
parent 04370b76d3
commit 95a46e3bb8

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

@ -2,7 +2,7 @@
> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/main/src/ch09-01-unrecoverable-errors-with-panic.md)
> <br>
> commit 426f3e4ec17e539ae9905ba559411169d303a031
> commit 199ca99926f232ee7f581a917eada4b65ff21754
突然有一天代码出问题了而你对此束手无策。对于这种情况Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug而且程序员并不清楚该如何处理它。
@ -20,20 +20,13 @@
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic,panics
fn main() {
panic!("crash and burn");
}
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-01-panic/src/main.rs}}
```
运行程序将会出现类似这样的输出:
```text
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
```console
{{#include ../listings/ch09-error-handling/no-listing-01-panic/output.txt}}
```
最后两行包含 `panic!` 调用造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:5* 表明这是 *src/main.rs* 文件的第二行第五个字符。
@ -47,81 +40,44 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic,panics
fn main() {
let v = vec![1, 2, 3];
v[99];
}
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-01/src/main.rs}}
```
<span class="caption">示例 9-1尝试访问超越 vector 结尾的元素,这会造成 `panic!`</span>
这里尝试访问 vector 的第一百个元素(这里的索引是 99 因为索引从 0 开始),不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。
这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 **缓冲区溢出***buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。
C 语言中尝试读取数据结构之后的值是未定义行为undefined behavior。你会得到任何对应数据结构中这个元素的内存位置的值甚至是这些内存并不属于这个数据结构的情况。这被称为 **缓冲区溢出***buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数据结构之后不被允许的数据。
为了使程序远离这类漏洞如果尝试读取一个索引不存在的元素Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:
为了保护程序远离这类漏洞如果尝试读取一个索引不存在的元素Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:
```text
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
note: Run with `RUST_BACKTRACE=1` for a backtrace.
```console
{{#include ../listings/ch09-error-handling/listing-09-01/output.txt}}
```
这指向了一个不是我们编写的文件,*libcore/slice/mod.rs*。其为 Rust 源码中 `slice` 的实现。这是当对 vector `v` 使用 `[]`*libcore/slice/mod.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。
接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace。*backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 `RUST_BACKTRACE` 环境变量设置为任何不是 0 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出:
错误指向 `main.rs` 的第 4 行,这里我们尝试访问索引 99。下面的说明note行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace。*backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 `RUST_BACKTRACE` 环境变量设置为任何不是 0 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出:
```text
```console
$ RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
1: std::sys_common::backtrace::print
at libstd/sys_common/backtrace.rs:71
at libstd/sys_common/backtrace.rs:59
2: std::panicking::default_hook::{{closure}}
at libstd/panicking.rs:211
3: std::panicking::default_hook
at libstd/panicking.rs:227
4: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::get
at libstd/panicking.rs:476
5: std::panicking::continue_panic_fmt
at libstd/panicking.rs:390
6: std::panicking::try::do_call
at libstd/panicking.rs:325
7: core::ptr::drop_in_place
at libcore/panicking.rs:77
8: core::ptr::drop_in_place
at libcore/panicking.rs:59
9: <usize as core::slice::SliceIndex<[T]>>::index
at libcore/slice/mod.rs:2448
10: core::slice::<impl core::ops::index::Index<I> for [T]>::index
at libcore/slice/mod.rs:2316
11: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
at liballoc/vec.rs:1653
12: panic::main
at src/main.rs:4
13: std::rt::lang_start::{{closure}}
at libstd/rt.rs:74
14: std::panicking::try::do_call
at libstd/rt.rs:59
at libstd/panicking.rs:310
15: macho_symbol_search
at libpanic_unwind/lib.rs:102
16: std::alloc::default_alloc_error_hook
at libstd/panicking.rs:289
at libstd/panic.rs:392
at libstd/rt.rs:58
17: std::rt::lang_start
at libstd/rt.rs:74
18: panic::main
0: rust_begin_unwind
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
1: core::panicking::panic_fmt
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
2: core::panicking::panic_bounds_check
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15
5: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982
6: panic::main
at ./src/main.rs:4
7: core::ops::function::FnOnce::call_once
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```
<span class="caption">示例 9-2当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息</span>
@ -133,4 +89,4 @@ stack backtrace:
本章后面的小节 [“panic! 还是不 panic!”][to-panic-or-not-to-panic] 会再次回到 `panic!` 并讲解何时应该、何时不应该使用 `panic!` 来处理错误情况。接下来,我们来看看如何使用 `Result` 来从错误中恢复。
[to-panic-or-not-to-panic]:
ch09-03-to-panic-or-not-to-panic.html#to-panic-or-not-to-panic
ch09-03-to-panic-or-not-to-panic.html#panic-还是不-panic

@ -2,14 +2,12 @@
> [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/main/src/ch09-02-recoverable-errors-with-result.md)
> <br>
> commit aa339f78da31c330ede3f1b52b4bbfb62d7814cb
> commit 0bac27c66136764c82fe267763945f3c65eea002
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。
回忆一下第二章 [“使用 `Result` 类型来处理潜在的错误”][handle_failure] 部分中的那个 `Result` 枚举,它定义有如下两个成员,`Ok` 和 `Err`
[handle_failure]: ch02-00-guessing-game-tutorial.html#handling-potential-failure-with-the-result-type
```rust
enum Result<T, E> {
Ok(T),
@ -24,11 +22,7 @@ enum Result<T, E> {
<span class="filename">文件名: src/main.rs</span>
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-03/src/main.rs}}
```
<span class="caption">示例 9-3打开文件</span>
@ -36,21 +30,13 @@ fn main() {
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看 [标准库 API 文档](https://doc.rust-lang.org/std/index.html)<!-- ignore -->,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下:
```rust,ignore,does_not_compile
let f: u32 = File::open("hello.txt");
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-02-ask-compiler-for-type/src/main.rs:here}}
```
现在尝试编译会给出如下输出:
```text
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
found type `std::result::Result<std::fs::File, std::io::Error>`
```console
{{#include ../listings/ch09-error-handling/no-listing-02-ask-compiler-for-type/output.txt}}
```
这就告诉我们了 `File::open` 函数的返回值类型是 `Result<T, E>`。这里泛型参数 `T` 放入了成功值的类型 `std::fs::File`,它是一个文件句柄。`E` 被用在失败值上时 `E` 的类型是 `std::io::Error`
@ -64,18 +50,7 @@ error[E0308]: mismatched types
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-04/src/main.rs}}
```
<span class="caption">示例 9-4使用 `match` 表达式处理可能会返回的 `Result` 成员</span>
@ -86,11 +61,11 @@ fn main() {
`match` 的另一个分支处理从 `File::open` 得到 `Err` 值的情况。在这种情况下,我们选择调用 `panic!` 宏。如果当前目录没有一个叫做 *hello.txt* 的文件,当运行这段代码时会看到如下来自 `panic!` 宏的输出:
```text
thread 'main' panicked at 'Problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
```console
{{#include ../listings/ch09-error-handling/listing-09-04/output.txt}}
```
一如既往,此输出准确地告诉了我们到底出了什么错。
### 匹配不同的错误
@ -100,23 +75,7 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-05/src/main.rs}}
```
<span class="caption">示例 9-5使用不同的方式处理不同类型的错误</span>
@ -125,26 +84,30 @@ fn main() {
我们希望在内层 `match` 中检查的条件是 `error.kind()` 的返回值是否为 `ErrorKind``NotFound` 成员。如果是,则尝试通过 `File::create` 创建文件。然而因为 `File::create` 也可能会失败,还需要增加一个内层 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外层 `match` 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。
这里有好多 `match``match` 确实很强大不过也非常的基础。第十三章我们会介绍闭包closure。`Result<T, E>` 有很多接受闭包的方法,并采用 `match` 表达式实现。一个更老练的 Rustacean 可能会这么写:
```rust,ignore
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = 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);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
```
虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 `match` 表达式且更容易阅读。在阅读完第十三章后再回到这个例子,并查看标准库文档 `unwrap_or_else` 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 `match` 表达式。
> 不同于使用 `match``Result<T, E>`
>
> 这里有好多 `match``match` 确实很强大不过也非常的基础。第十三章我们会介绍闭包closure这可以用于很多 `Result<T, E>` 上定义的方法。在处理代码中的 `Result<T, E>` 值时这些方法可能会更加简洁。
>
> 例如,这是另一个编写与示例 9-5 逻辑相同但是使用闭包和 `unwrap_or_else` 方法的例子:
>
> ```rust,ignore
> use std::fs::File;
> use std::io::ErrorKind;
>
> fn main() {
> let f = 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);
> })
> } else {
> panic!("Problem opening the file: {:?}", error);
> }
> });
> }
> ```
>
> 虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 `match` 表达式且更容易阅读。在阅读完第十三章后再回到这个例子,并查看标准库文档 `unwrap_or_else` 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 `match` 表达式。
### 失败时 panic 的简写:`unwrap` 和 `expect`
@ -153,11 +116,7 @@ fn main() {
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-04-unwrap/src/main.rs}}
```
如果调用这段代码时不存在 *hello.txt* 文件,我们将会看到一个 `unwrap` 调用 `panic!` 时提供的错误信息:
@ -173,11 +132,7 @@ src/libcore/result.rs:906:4
<span class="filename">文件名: src/main.rs</span>
```rust,should_panic
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-05-expect/src/main.rs}}
```
`expect``unwrap` 的使用方式一样:返回文件句柄或调用 `panic!` 宏。`expect` 在调用 `panic!` 时使用的错误信息将是我们传递给 `expect` 的参数,而不像 `unwrap` 那样使用默认的 `panic!` 信息。它看起来像这样:
@ -198,34 +153,16 @@ thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
<span class="filename">文件名: src/main.rs</span>
```rust
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
{{#include ../listings/ch09-error-handling/listing-09-06/src/main.rs:here}}
```
<span class="caption">示例 9-6一个函数使用 `match` 将错误返回给代码调用者</span>
首先让我们看看函数的返回值:`Result<String, io::Error>`。这意味着函数返回一个 `Result<T, E>` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String``Ok` 值 —— 函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。
函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-4 中的 `match`唯一的区别是当 `Err` 时不再调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。
函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-4 中的 `match`,如果 `File::open` 成功了,模式中变量 `file` 中的文件句柄会变成可变变量 `f` 的值同时函数继续。`Err` 的情况下,不再调用 `panic!`,而是使用 `return` 关键字提早返回整个函数并将 `File::open` 返回的模式中变量 `e` 中的错误值作为函数的错误返回值传递给调用者。
接着我们在变量 `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`,因为这是函数的最后一个表达式。
所以 `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`,因为这是函数的最后一个表达式。
调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error``Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。
@ -238,16 +175,7 @@ fn read_username_from_file() -> Result<String, io::Error> {
<span class="filename">文件名: src/main.rs</span>
```rust
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
{{#include ../listings/ch09-error-handling/listing-09-07/src/main.rs:here}}
```
<span class="caption">示例 9-7一个使用 `?` 运算符向调用者返回错误的函数</span>
@ -263,86 +191,76 @@ fn read_username_from_file() -> Result<String, io::Error> {
<span class="filename">文件名: src/main.rs</span>
```rust
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
{{#include ../listings/ch09-error-handling/listing-09-08/src/main.rs:here}}
```
<span class="caption">示例 9-8问号运算符之后的链式方法调用</span>
`s` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open``read_to_string` 都成功没有失败时返回包含用户名 `s``Ok` 值。其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学(ergonomic)的写法。
`s` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open``read_to_string` 都成功没有失败时返回包含用户名 `s``Ok` 值。其功能再一次与示例 9-6 和示例 9-7 保持一致不过这是一个与众不同且更符合工程学ergonomic的写法。
说到编写这个函数的不同方法,甚至还有一个更短的写法:
<span class="filename">文件名: src/main.rs</span>
```rust
use std::io;
use std::fs;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
{{#include ../listings/ch09-error-handling/listing-09-09/src/main.rs:here}}
```
<span class="caption">示例 9-9: 使用 `fs::read_to_string`</span>
将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 `fs::read_to_string` 的函数,它会打开文件、新建一个 `String`、读取文件的内容,并将内容放入 `String`,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。
### `?` 运算符可被用于返回 `Result` 的函数
### 哪里可以使用 `?` 运算符
`?` 运算符可被用于返回值类型为 `Result` 的函数,因为他被定义为与示例 9-6 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。
`?` 运算符只能被用于返回值与 `?` 作用的值相兼容的函数。因为 `?` 运算符被定义为从函数中提早返回一个值,这与示例 9-6 中的 `match` 表达式有着完全相同的工作方式。示例 9-6 中 `match` 作用于一个 `Result` 值,提早返回的分支返回了一个 `Err(e)` 值。函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。
让我们看看在 `main` 函数中使用 `?` 运算符会发生什么,如果你还记得的话其返回值类型是`()`
在示例 9-10 中,让我们看看在返回值不兼容的 `main` 函数中使用 `?` 运算符会得到什么错误
```rust,ignore,does_not_compile
use std::fs::File;
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-10/src/main.rs}}
```
fn main() {
let f = File::open("hello.txt")?;
}
<span class="caption">示例 9-10: 尝试在返回 `()``main` 函数中使用 `?` 的代码不能编译</span>
这段代码打开一个文件,这可能会失败。`?` 运算符作用于 `File::open` 返回的 `Result` 值,不过 `main` 函数的返回类型是 `()` 而不是 `Result`。当编译这些代码,会得到如下错误信息:
```console
{{#include ../listings/ch09-error-handling/listing-09-10/output.txt}}
```
当编译这些代码,会得到如下错误信息:
这个错误指出只能在返回 `Result` 或者其它实现了 `FromResidual` 的类型的函数中使用 `?` 运算符。为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为 `Result<T, E>`。另一个是使用 `match``Result<T, E>` 的方法中合适的一个来处理 `Result<T, E>`
```text
error[E0277]: the `?` operator can only be used in a function that returns
`Result` or `Option` (or another type that implements `std::ops::Try`)
--> src/main.rs:4:13
|
4 | let f = File::open("hello.txt")?;
| ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a
function that returns `()`
|
= help: the trait `std::ops::Try` is not implemented for `()`
= note: required by `std::ops::Try::from_error`
错误信息也提到 `?` 也可用于 `Option<T>` 值。如同对 `Result` 使用 `?` 一样,只能在返回 `Option` 的函数中对 `Option` 使用 `?`。在 `Option<T>` 上调用 `?` 运算符的行为与 `Result<T, E>` 类似:如果值是 `None`,此时 `None` 会从函数中提前返回。如果值是 `Some``Some` 中的值作为表达式的返回值同时函数继续。示例 9-11 中有一个从给定文本中返回第一行最后一个字符的函数的例子:
```rust
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-11/src/main.rs:here}}
```
<span class="caption">示例 9-11: 在 `Option<T>` 值上使用 `?` 运算符</span>
错误指出只能在返回 `Result` 或者其它实现了 `std::ops::Try` 的类型的函数中使用 `?` 运算符。当你期望在不返回 `Result` 的函数中调用其他返回 `Result` 的函数时使用 `?` 的话,有两种方法修复这个问题。一种技巧是将函数返回值类型修改为 `Result<T, E>`,如果没有其它限制阻止你这么做的话。另一种技巧是通过合适的方法使用 `match``Result` 的方法之一来处理 `Result<T, E>`
这个函数返回 `Option<char>` 因为它可能会在这个位置找到一个字符,也可能没有字符。这段代码获取 `text` 字符串 slice 作为参数并调用其 `lines` 方法,这会返回一个字符串中每一行的迭代器。因为函数希望检查第一行,所以调用了迭代器 `next` 来获取迭代器中第一个值。如果 `text` 是空字符串,`next` 调用会返回 `None`,此时我们可以使用 `?` 来停止并从 `last_char_of_first_line` 返回 `None`。如果 `text` 不是空字符串,`next` 会返回一个包含 `text` 中第一行的字符串 slice 的 `Some` 值。
`main` 函数是特殊的,其必须返回什么类型是有限制的。`main` 函数的一个有效的返回值是 `()`,同时出于方便,另一个有效的返回值是 `Result<(), E>`,如下所示:
`?` 会提取这个字符串 slice然后可以在字符串 slice 上调用 `chars` 来获取字符的迭代器。我们感兴趣的是第一行的最后一个字符,所以可以调用 `last` 来返回迭代器的最后一项。这是一个 `Option`,因为有可能第一行是一个空字符串,例如 `text` 以一个空行开头而后面的行有文本,像是 `"\nhi"`。不过,如果第一行有最后一个字符,它会返回在一个 `Some` 成员中。`?` 运算符作用于其中给了我们一个简介的表达这种逻辑的方式。如果我们不能在 `Option` 上使用 `?` 运算符,则不得不使用更多的方法调用或者 `match` 表达式来实现这些逻辑。
```rust,ignore
use std::error::Error;
use std::fs::File;
注意你可以在返回 `Result` 的函数中对 `Result` 使用 `?` 运算符,可以在返回 `Option` 的函数中对 `Option` 使用 `?` 运算符,但是不可以混合搭配。`?` 运算符不会自动将 `Result` 转化为 `Option`,反之亦然;在这些情况下,可以使用类似 `Result``ok` 方法或者 `Option``ok_or` 方法来显式转换。
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
目前为止,我们所使用的所有 `main` 函数都返回 `()`。`main` 函数是特殊的因为它是可执行程序的入口点和退出点,为了使程序能正常工作其可以返回的类型使有限制的。
Ok(())
}
幸运的是 `main` 函数也可以返回 `Result<(), E>` 示例 9-12 中的代码来自示例 9-10 不过修改了 `main` 的返回值为 `Result<(), Box<dyn Error>>` 并在结尾增加了一个 `Ok(())` 作为返回值。这段代码可以编译:
```rust,ignore
{{#rustdoc_include ../listings/ch09-error-handling/listing-09-12/src/main.rs}}
```
`Box<dyn Error>` 被称为 “trait 对象”“trait object”第十七章 [“为使用不同类型的值而设计的 trait 对象”][trait-objects] 部分会做介绍。目前可以理解 `Box<dyn Error>` 为使用 `?``main` 允许返回的 “任何类型的错误”。
<span class="caption">示例 9-12: 修改 `main` 返回 `Result<(), E>` 允许对 `Result` 值使用 `?` 运算符</span>
`Box<dyn Error>` 类型是一个 **trait 对象***trait object*)第十七章 [“为使用不同类型的值而设计的 trait 对象”][trait-objects] 部分会做介绍。目前可以将 `Box<dyn Error>` 理解为 “任何类型的错误”。在返回 `Box<dyn Error>` 错误类型 `main` 函数中对 `Result` 使用 `?` 是允许的,因为它允许任何 `Err` 值提前返回。
`main` 函数返回 `Result<(), E>`,如果 `main` 返回 `Ok(())` 可执行程序会以 `0` 值退出,而如果 `main` 返回 `Err` 值则会以非零值退出;成功退出的程序会返回整数 `0`,运行错误的程序会返回非 `0` 的整数。Rust 也会从二进制程序中返回与这个惯例相兼容的整数。
`main` 函数也可以返回任何实现了 [`std::process::Termination` trait][termination] 的类型。截至本书编写时,`Termination` trait 是一个不稳定功能unstable feature只能用于 Nightly Rust 中,所以你不能在 稳定版 RustStable Rust中用自己的类型去实现不过有朝一日应该可以
现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到他们各自适合哪些场景的话题了。
[trait-objects]: ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types
[handle_failure]: ch02-00-guessing-game-tutorial.html#使用-result-类型来处理潜在的错误
[trait-objects]: ch17-02-trait-objects.html#为使用不同类型的值而设计的-trait-对象
[termination]: https://doc.rust-lang.org/std/process/trait.Termination.html

@ -2,11 +2,11 @@
> [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)
> <br>
> commit 76df60bccead5f3de96db23d97b69597cd8a2b82
> commit 199ca99926f232ee7f581a917eada4b65ff21754
那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。
有一些情况 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的。章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
在一些类似示例、原型代码prototype code和测试中 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的。章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
### 示例、代码原型和测试都非常适合 panic
@ -21,9 +21,7 @@
当你有一些其他的逻辑来确保 `Result` 会是 `Ok` 值时,调用 `unwrap` 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 `Result` 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 `Err` 值,那么调用 `unwrap` 也是完全可以接受的,这里是一个例子:
```rust
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-08-unwrap-that-cant-fail/src/main.rs:here}}
```
我们通过解析一个硬编码的字符来创建一个 `IpAddr` 实例。可以看出 `127.0.0.1` 是一个有效的 IP 地址,所以这里使用 `unwrap` 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 `parse` 方法的返回值类型:它仍然是一个 `Result` 值,而编译器仍然会要求我们处理这个 `Result`,好像还是有可能出现 `Err` 成员那样。这是因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 **确实** 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 `Result` 了。
@ -32,9 +30,9 @@ let home: IpAddr = "127.0.0.1".parse().unwrap();
在当有可能会导致有害状态的情况下建议使用 `panic!` —— 在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值 —— 外加如下几种情况:
* 有害状态并不包含 **预期** 会偶尔发生的错误
* 在此之后代码的运行依赖于不处于这种有害状态
* 没有可行的手段来将有害状态信息编码进所使用的类型中的情况
* 有害状态是非预期的行为,与偶尔会发生的行为相对,比如用户输入了错误格式的数据。
* 在此之后代码的运行依赖于不处于这种有害状态,而不是在每一步都检查是否有问题。
* 没有可行的手段来将有害状态信息编码进所使用的类型中的情况。我们会在第十七章 [“将状态和行为编码为类型”][encoding] 部分通过一个例子来说明我们的意思。
如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 `panic!` 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,如果你正在调用不受你控制的外部代码,并且它返回了一个你无法修复的无效状态,那么 `panic!` 往往是合适的。
@ -51,53 +49,20 @@ let home: IpAddr = "127.0.0.1".parse().unwrap();
一种实现方式是将猜测解析成 `i32` 而不仅仅是 `u32`,来默许输入负数,接着检查数字是否在范围内:
```rust,ignore
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
}
{{#rustdoc_include ../listings/ch09-error-handling/no-listing-09-guess-out-of-range/src/main.rs:here}}
```
`if` 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用 `continue` 开始下一次循环,请求另一个猜测。`if` 表达式之后,就可以在知道 `guess` 在 1 到 100 之间的情况下与秘密数字作比较了。
然而,这并不是一个理想的解决方案:如果让程序仅仅处理 1 到 100 之间的值是一个绝对需要满足的要求,而且程序中的很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-10 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-13 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
```rust
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess {
value
}
}
pub fn value(&self) -> i32 {
self.value
}
}
{{#include ../listings/ch09-error-handling/listing-09-13/src/main.rs:here}}
```
<span class="caption">示例 9-10:一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span>
<span class="caption">示例 9-13一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span>
首先,我们定义了一个包含 `i32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。
@ -112,3 +77,5 @@ impl Guess {
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!``Result` 将会使你的代码在面对不可避免的错误时显得更加可靠。
现在我们已经见识过了标准库中 `Option``Result` 泛型枚举的能力了,在下一章让我们聊聊泛型是如何工作的,以及如何在你的代码中使用他们。
[encoding]: ch17-03-oo-design-patterns.html#将状态和行为编码为类型

Loading…
Cancel
Save