mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
check ch10-01
This commit is contained in:
parent
c5a42db0bf
commit
617ece0b98
@ -6,6 +6,6 @@
|
||||
|
||||
Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。
|
||||
|
||||
Rust 将错误组合成两个主要类别:**可恢复错误**(*recoverable*)和**不可恢复错误**(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
|
||||
Rust 将错误组合成两个主要类别:**可恢复错误**(*recoverable*)和 **不可恢复错误**(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
|
||||
|
||||
大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有`Result<T, E>`值和`panic!`,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍`panic!`调用,接着会讲到如何返回`Result<T, E>`。最后,我们会讨论当决定是尝试从错误中恢复还是停止执行时需要顾及的权衡考虑。
|
||||
大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有 `Result<T, E>` 值,以及 `panic!`,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍 `panic!` 调用,接着会讲到如何返回 `Result<T, E>`。最后,我们会讨论当决定是尝试从错误中恢复还是停止执行时需要顾及的权衡考虑。
|
@ -1,23 +1,23 @@
|
||||
## `panic!`与不可恢复的错误
|
||||
## `panic!` 与不可恢复的错误
|
||||
|
||||
> [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)
|
||||
> <br>
|
||||
> commit e26bb338ab14b98a850c3464e821d54940a45672
|
||||
> commit 8d24b2a5e61b4eea109d26e38d2144408ae44e53
|
||||
|
||||
突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。
|
||||
突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。
|
||||
|
||||
> ### Panic 中的栈展开与终止
|
||||
>
|
||||
> 当出现`panic!`时,程序默认会开始**展开**(*unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接**终止**(*abort*),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 *Cargo.toml* 的`[profile]`部分增加`panic = 'abort'`。例如,如果你想要在发布模式中 panic 时直接终止:
|
||||
> 当出现 `panic!` 时,程序默认会开始 **展开**(*unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 **终止**(*abort*),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 *Cargo.toml* 的 `[profile]` 部分增加 `panic = 'abort'`。例如,如果你想要在发布模式中 panic 时直接终止:
|
||||
>
|
||||
> ```toml
|
||||
> [profile.release]
|
||||
> panic = 'abort'
|
||||
> ```
|
||||
|
||||
让我们在一个简单的程序中调用`panic!`:
|
||||
让我们在一个简单的程序中调用 `panic!`:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,should_panic
|
||||
fn main() {
|
||||
@ -27,25 +27,25 @@ fn main() {
|
||||
|
||||
运行程序将会出现类似这样的输出:
|
||||
|
||||
```
|
||||
```text
|
||||
$ cargo run
|
||||
Compiling panic v0.1.0 (file:///projects/panic)
|
||||
Finished debug [unoptimized + debuginfo] target(s) in 0.25 secs
|
||||
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
|
||||
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* 表明这是 *src/main.rs* 文件的第二行。
|
||||
|
||||
在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现`panic!`宏的调用。换句话说,`panic!`可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的`panic!`宏调用,而不是我们代码中最终导致`panic!`的那一行。可以使用`panic!`被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。
|
||||
在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 `panic!` 宏的调用。换句话说,`panic!` 可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。可以使用 `panic!` 被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。
|
||||
|
||||
### 使用`panic!`的 backtrace
|
||||
### 使用 `panic!` 的 backtrace
|
||||
|
||||
让我们来看看另一个因为我们代码中的 bug 引起的别的库中`panic!`的例子,而不是直接的宏调用:
|
||||
让我们来看看另一个因为我们代码中的 bug 引起的别的库中 `panic!` 的例子,而不是直接的宏调用:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,should_panic
|
||||
fn main() {
|
||||
@ -55,16 +55,16 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。`[]`应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。
|
||||
这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。
|
||||
|
||||
这种情况下其他像 C 这样语言会尝直接试提供所要求的值,即便这可能不是你期望的:你会得到对任何应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为**缓冲区溢出**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。
|
||||
这种情况下其他像 C 这样语言会尝直接试提供所要求的值,即便这可能不是你期望的:你会得到对任何应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 **缓冲区溢出**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。
|
||||
|
||||
为了使程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:
|
||||
|
||||
```
|
||||
```text
|
||||
$ cargo run
|
||||
Compiling panic v0.1.0 (file:///projects/panic)
|
||||
Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs
|
||||
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
|
||||
@ -72,13 +72,13 @@ 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<T>`的实现。这是当对 vector `v`使用`[]`时 *libcollections/vec.rs* 中会执行的代码,也是真正出现`panic!`的地方。
|
||||
这指向了一个不是我们编写的文件,*libcollections/vec.rs*。这是标准库中 `Vec<T>` 的实现。这是当对 vector `v` 使用 `[]` 时 *libcollections/vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。
|
||||
|
||||
接下来的几行提醒我们可以设置`RUST_BACKTRACE`环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:
|
||||
接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:
|
||||
|
||||
```
|
||||
```text
|
||||
$ RUST_BACKTRACE=1 cargo run
|
||||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||||
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
|
||||
stack backtrace:
|
||||
@ -116,11 +116,10 @@ stack backtrace:
|
||||
17: 0x0 - <unknown>
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-1: The backtrace generated by a call to
|
||||
`panic!` displayed when the environment variable `RUST_BACKTRACE` is set</span>
|
||||
<span class="caption">列表 9-1:当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息</span>
|
||||
|
||||
这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:*src/main.rs* 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。
|
||||
|
||||
如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你得代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。
|
||||
如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。
|
||||
|
||||
本章的后面会再次回到`panic!`并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用`Result`来从错误中恢复。
|
||||
本章的后面会再次回到 `panic!` 并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用 `Result` 来从错误中恢复。
|
@ -1,12 +1,12 @@
|
||||
## `Result`与可恢复的错误
|
||||
## `Result` 与可恢复的错误
|
||||
|
||||
> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-02-recoverable-errors-with-result.md)
|
||||
> <br>
|
||||
> commit e6d6caab41471f7115a621029bd428a812c5260e
|
||||
|
||||
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这时我们可能想要创建这个文件而不是终止进程。
|
||||
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作失败,这时我们可能想要创建这个文件而不是终止进程。
|
||||
|
||||
回忆一下第二章“使用`Result`类型来处理潜在的错误”部分中的那个`Result`枚举,它定义有如下两个成员,`Ok`和`Err`:
|
||||
回忆一下第二章 “使用 `Result` 类型来处理潜在的错误” 部分中的那个 `Result` 枚举,它定义有如下两个成员,`Ok` 和 `Err`:
|
||||
|
||||
[handle_failure]: ch02-00-guessing-game-tutorial.html#handling-potential-failure-with-the-result-type
|
||||
|
||||
@ -17,11 +17,11 @@ enum Result<T, E> {
|
||||
}
|
||||
```
|
||||
|
||||
`T`和`E`是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是`T`代表成功时返回的`Ok`成员中的数据的类型,而`E`代表失败时返回的`Err`成员中的错误的类型。因为`Result`有这些泛型类型参数,我们可以将`Result`类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
|
||||
`T` 和 `E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
|
||||
|
||||
让我们调用一个返回`Result`的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:
|
||||
让我们调用一个返回 `Result` 的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
use std::fs::File;
|
||||
@ -31,9 +31,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-2: Opening a file</span>
|
||||
<span class="caption">列表 9-2:打开文件</span>
|
||||
|
||||
如何知道`File::open`返回一个`Result`呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给`f`某个我们知道**不是**函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们`f`的类型**应该**是什么,为此我们将`let f`语句改为:
|
||||
如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么,为此我们将 `let f` 语句改为:
|
||||
|
||||
```rust,ignore
|
||||
let f: u32 = File::open("hello.txt");
|
||||
@ -41,7 +41,7 @@ let f: u32 = File::open("hello.txt");
|
||||
|
||||
现在尝试编译会给出如下错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error[E0308]: mismatched types
|
||||
--> src/main.rs:4:18
|
||||
|
|
||||
@ -53,15 +53,15 @@ error[E0308]: mismatched types
|
||||
= note: found type `std::result::Result<std::fs::File, std::io::Error>`
|
||||
```
|
||||
|
||||
这就告诉我们了`File::open`函数的返回值类型是`Result<T, E>`。这里泛型参数`T`放入了成功值的类型`std::fs::File`,它是一个文件句柄。`E`被用在失败值上其类型是`std::io::Error`。
|
||||
这就告诉我们了 `File::open` 函数的返回值类型是 `Result<T, E>`。这里泛型参数 `T` 放入了成功值的类型 `std::fs::File`,它是一个文件句柄。`E` 被用在失败值上时其类型是 `std::io::Error`。
|
||||
|
||||
这个返回值类型说明`File::open`调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。`File::open`需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是`Result`枚举可以提供的。
|
||||
这个返回值类型说明 `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-2 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。列表 9-3 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,should_panic
|
||||
use std::fs::File;
|
||||
@ -78,25 +78,24 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-3: Using a `match` expression to handle the
|
||||
`Result` variants we might have</span>
|
||||
<span class="caption">列表 9-3:使用 `match` 表达式处理可能的 `Result` 成员</span>
|
||||
|
||||
注意与`Option`枚举一样,`Result`枚举和其成员也被导入到了 prelude 中,所以就不需要在`match`分支中的`Ok`和`Err`之前指定`Result::`。
|
||||
注意与 `Option` 枚举一样,`Result` 枚举和其成员也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok` 和 `Err` 之前指定 `Result::`。
|
||||
|
||||
这里我们告诉 Rust 当结果是`Ok`,返回`Ok`成员中的`file`值,然后将这个文件句柄赋值给变量`f`。`match`之后,我们可以利用这个文件句柄来进行读写。
|
||||
这里我们告诉 Rust 当结果是 `Ok` 时,返回 `Ok` 成员中的 `file` 值,然后将这个文件句柄赋值给变量 `f`。`match` 之后,我们可以利用这个文件句柄来进行读写。
|
||||
|
||||
`match`的另一个分支处理从`File::open`得到`Err`值的情况。在这种情况下,我们选择调用`panic!`宏。如果当前目录没有一个叫做 *hello.txt* 的文件,当运行这段代码时会看到如下来自`panic!`宏的输出:
|
||||
`match` 的另一个分支处理从 `File::open` 得到 `Err` 值的情况。在这种情况下,我们选择调用 `panic!` 宏。如果当前目录没有一个叫做 *hello.txt* 的文件,当运行这段代码时会看到如下来自 `panic!` 宏的输出:
|
||||
|
||||
```
|
||||
```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
|
||||
```
|
||||
|
||||
### 匹配不同的错误
|
||||
|
||||
列表 9-3 中的代码不管`File::open`是因为什么原因失败都会`panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果`File::open`因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果`File::open`因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样`panic!`。让我们看看列表 9-4,其中`match`增加了另一个分支:
|
||||
列表 9-3 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样 `panic!`。让我们看看列表 9-4,其中 `match` 增加了另一个分支:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
use std::fs::File;
|
||||
@ -128,18 +127,17 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-4: Handling different kinds of errors in
|
||||
different ways</span>
|
||||
<span class="caption">列表 9-4:使用不同的方式处理不同类型的错误</span>
|
||||
|
||||
`File::open`返回的`Err`成员中的值类型`io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回`io::ErrorKind`值的`kind`方法可供调用。`io::ErrorKind`是一个标准库提供的枚举,它的成员对应`io`操作可能导致的不同错误类型。我们感兴趣的成员是`ErrorKind::NotFound`,它代表尝试打开的文件并不存在。
|
||||
`File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。
|
||||
|
||||
条件`if error.kind() == ErrorKind::NotFound`被称作 *match guard*:它是一个进一步完善`match`分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑`match`中的下一个分支。模式中的`ref`是必须的,这样`error`就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用`ref`而不是`&`来获取一个引用。简而言之,在模式的上下文中,`&`匹配一个引用并返回它的值,而`ref`匹配一个值并返回一个引用。
|
||||
条件 `if error.kind() == ErrorKind::NotFound` 被称作 *match guard*:它是一个进一步完善 `match` 分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑 `match` 中的下一个分支。模式中的 `ref` 是必须的,这样 `error` 就不会被移动到 guard 条件中而仅仅只是引用它。第十八章会详细解释为什么在模式中使用 `ref` 而不是 `&` 来获取一个引用。简而言之,在模式的上下文中,`&` 匹配一个引用并返回它的值,而 `ref` 匹配一个值并返回一个引用。
|
||||
|
||||
在 match guard 中我们想要检查的条件是`error.kind()`是否是`ErrorKind`枚举的`NotFound`成员。如果是,尝试用`File::create`创建文件。然而`File::create`也可能会失败,我们还需要增加一个内部`match`语句。当文件不能被打开,会打印出一个不同的错误信息。外部`match`的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。
|
||||
在 match guard 中我们想要检查的条件是 `error.kind()` 是否是 `ErrorKind` 枚举的 `NotFound` 成员。如果是,尝试用 `File::create` 创建文件。然而 `File::create` 也可能会失败,还需要增加一个内部 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 `match` 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。
|
||||
|
||||
### 失败时 panic 的捷径:`unwrap`和`expect`
|
||||
### 失败时 panic 的捷径:`unwrap` 和 `expect`
|
||||
|
||||
`match`能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。`Result<T, E>`类型定义了很多辅助方法来处理各种情况。其中之一叫做`unwrap`,它的实现就类似于列表 9-3 中的`match`语句。如果`Result`值是成员`Ok`,`unwrap`会返回`Ok`中的值。如果`Result`是成员`Err`,`unwrap`会为我们调用`panic!`。
|
||||
`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明意图。`Result<T, E>` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于列表 9-3 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。
|
||||
|
||||
```rust,should_panic
|
||||
use std::fs::File;
|
||||
@ -149,15 +147,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
如果调用这段代码时不存在 *hello.txt* 文件,我们将会看到一个`unwrap`调用`panic!`时提供的错误信息:
|
||||
如果调用这段代码时不存在 *hello.txt* 文件,我们将会看到一个 `unwrap` 调用 `panic!` 时提供的错误信息:
|
||||
|
||||
```
|
||||
```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
|
||||
```
|
||||
|
||||
还有另一个类似于`unwrap`的方法它还允许我们选择`panic!`的错误信息:`expect`。使用`expect`而不是`unwrap`并提供一个好的错误信息可以表明你的意图并有助于追踪 panic 的根源。`expect`的语法看起来像这样:
|
||||
还有另一个类似于 `unwrap` 的方法它还允许我们选择 `panic!` 的错误信息:`expect`。使用 `expect` 而不是 `unwrap` 并提供一个好的错误信息可以表明你的意图并有助于追踪 panic 的根源。`expect` 的语法看起来像这样:
|
||||
|
||||
```rust,should_panic
|
||||
use std::fs::File;
|
||||
@ -167,9 +165,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
`expect`与`unwrap`的使用方式一样:返回文件句柄或调用`panic!`宏。`expect`用来调用`panic!`的错误信息将会作为传递给`expect`的参数,而不像`unwrap`那样使用默认的`panic!`信息。它看起来像这样:
|
||||
`expect` 与 `unwrap` 的使用方式一样:返回文件句柄或调用 `panic!` 宏。`expect` 用来调用 `panic!` 的错误信息将会作为参数传递给 `expect` ,而不像`unwrap` 那样使用默认的 `panic!` 信息。它看起来像这样:
|
||||
|
||||
```
|
||||
```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
|
||||
@ -177,7 +175,7 @@ thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
|
||||
|
||||
### 传播错误
|
||||
|
||||
当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为**传播**(*propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
|
||||
当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 **传播**(*propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
|
||||
|
||||
例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
|
||||
|
||||
@ -203,22 +201,21 @@ fn read_username_from_file() -> Result<String, io::Error> {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-5: A function that returns errors to the
|
||||
calling code using `match`</span>
|
||||
<span class="caption">列表 9-5:一个函数使用 `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`方法。
|
||||
首先让我们看看函数的返回值:`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-3 中的`match`,唯一的区别是不再当`Err`时调用`panic!`,而是提早返回并将`File::open`返回的错误值作为函数的错误返回值传递给调用者。如果`File::open`成功了,我们将文件句柄储存在变量`f`中并继续。
|
||||
函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于列表 9-3 中的 `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!`并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。
|
||||
调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error` 的 `Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。
|
||||
|
||||
这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:`?`。
|
||||
|
||||
### 传播错误的捷径:`?`
|
||||
|
||||
列表 9-6 展示了一个`read_username_from_file`的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的:
|
||||
列表 9-6 展示了一个 `read_username_from_file` 的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的:
|
||||
|
||||
```rust
|
||||
use std::io;
|
||||
@ -233,14 +230,13 @@ fn read_username_from_file() -> Result<String, io::Error> {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-6: A function that returns errors to the
|
||||
calling code using `?`</span>
|
||||
<span class="caption">列表 9-6:一个使用 `?` 向调用者返回错误的函数</span>
|
||||
|
||||
`Result`值之后的`?`被定义为与列表 9-5 中定义的处理`Result`值的`match`表达式有着完全相同的工作方式。如果`Result`的值是`Ok`,这个表达式将会返回`Ok`中的值而程序将继续执行。如果值是`Err`,`Err`中的值将作为整个函数的返回值,就好像使用了`return`关键字一样,这样错误值就被传播给了调用者。
|
||||
`Result` 值之后的 `?` 被定义为与列表 9-5 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。
|
||||
|
||||
在列表 9-6 的上下文中,`File::open`调用结尾的`?`将会把`Ok`中的值返回给变量`f`。如果出现了错误,`?`会提早返回整个函数并将任何`Err`值传播给调用者。同理也适用于`read_to_string`调用结尾的`?`。
|
||||
在列表 9-6 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将任何 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`。
|
||||
|
||||
`?`消除了大量样板代码并使得函数的实现更简单。我们甚至可以在`?`之后直接使用链式方法调用来进一步缩短代码:
|
||||
`?` 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码:
|
||||
|
||||
```rust
|
||||
use std::io;
|
||||
@ -256,13 +252,13 @@ fn read_username_from_file() -> Result<String, io::Error> {
|
||||
}
|
||||
```
|
||||
|
||||
在`s`中创建新的`String`被放到了函数开头;这没有什么变化。我们对`File::open("hello.txt")?`的结果直接链式调用了`read_to_string`,而不再创建变量`f`。仍然需要`read_to_string`调用结尾的`?`,而且当`File::open`和`read_to_string`都成功没有失败时返回包含用户名`s`的`Ok`值。其功能再一次与列表 9-5 和列表 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。
|
||||
在 `s` 中创建新的 `String` 被放到了函数开头;这没有什么变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `s` 的 `Ok` 值。其功能再一次与列表 9-5 和列表 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。
|
||||
|
||||
### `?`只能被用于返回`Result`的函数
|
||||
### `?` 只能被用于返回 `Result` 的函数
|
||||
|
||||
`?`只能被用于返回值类型为`Result`的函数,因为他被定义为与列表 9-5 中的`match`表达式有着完全相同的工作方式。`match`的`return Err(e)`部分要求返回值类型是`Result`,所以函数的返回值必须是`Result`才能与这个`return`相兼容。
|
||||
`?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与列表 9-5 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。
|
||||
|
||||
让我们看看在`main`函数中使用`?`会发生什么,如果你还记得的话它的返回值类型是`()`:
|
||||
让我们看看在 `main` 函数中使用 `?` 会发生什么,如果你还记得的话它的返回值类型是`()`:
|
||||
|
||||
```rust,ignore
|
||||
use std::fs::File;
|
||||
@ -276,7 +272,6 @@ fn main() {
|
||||
that doesn't return a result is STILL confusing. Since we want to only explain
|
||||
`?` now, I've changed the example, but if you try running this code you WON'T
|
||||
get the error message below.
|
||||
|
||||
I'm bugging people to try and get
|
||||
https://github.com/rust-lang/rust/issues/35946 fixed soon, hopefully before this
|
||||
chapter gets through copy editing-- at that point I'll make sure to update this
|
||||
@ -284,7 +279,7 @@ error message. /Carol -->
|
||||
|
||||
当编译这些代码,会得到如下错误信息:
|
||||
|
||||
```
|
||||
```text
|
||||
error[E0308]: mismatched types
|
||||
-->
|
||||
|
|
||||
@ -296,6 +291,6 @@ error[E0308]: mismatched types
|
||||
= note: found type `std::result::Result<_, _>`
|
||||
```
|
||||
|
||||
错误指出存在不匹配的类型:`main`函数返回一个`()`类型,而`?`返回一个`Result`。编写不返回`Result`的函数时,如果调用其他返回`Result`的函数,需要使用`match`或者`Result`的方法之一来处理它,而不能用`?`将潜在的错误传播给调用者。
|
||||
错误指出存在不匹配的类型:`main` 函数返回一个 `()` 类型,而 `?` 返回一个 `Result`。编写不返回 `Result` 的函数时,如果调用其他返回 `Result` 的函数,需要使用 `match` 或者 `Result` 的方法之一来处理它,而不能用 `?` 将潜在的错误传播给调用者。
|
||||
|
||||
现在我们讨论过了调用`panic!`或返回`Result`的细节,是时候返回他们各自适合哪些场景的话题了。
|
||||
现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候返回他们各自适合哪些场景的话题了。
|
||||
|
@ -1,24 +1,24 @@
|
||||
## `panic!`还是不`panic!`
|
||||
## `panic!` 还是不 `panic!`
|
||||
|
||||
> [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)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit 88dee413c0792dcaf563d753e00400cc191f1fbe
|
||||
|
||||
那么,该如何决定何时应该`panic!`以及何时应该返回`Result`呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用`panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回`Result`值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为`Err`是不可恢复的,所以他们也可能会调用`panic!`并将可恢复的错误变成了不可恢复的错误。因此返回`Result`是定义可能会失败的函数的一个好的默认选择。
|
||||
那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。
|
||||
|
||||
有一些情况 panic 比返回`Result`更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
|
||||
有一些情况 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。
|
||||
|
||||
### 示例、代码原型和测试:非常适合 panic
|
||||
|
||||
当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似`unwrap`这样可能`panic!`的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。
|
||||
当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似 `unwrap` 这样可能 `panic!` 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。
|
||||
|
||||
类似的,`unwrap`和`expect`方法在原型设计时非常方便,在你决定该如何处理错误之前。他们在代码中留下了明显的记号,以便你准备使程序变得更健壮时作为参考。
|
||||
类似的,`unwrap` 和 `expect` 方法在原型设计时非常方便,在你决定该如何处理错误之前。他们在代码中留下了明显的记号,以便你准备使程序变得更健壮时作为参考。
|
||||
|
||||
如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为`panic!`是测试如何被标记为失败的,调用`unwrap`或`expect`都是非常有道理的。
|
||||
如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 `panic!` 是测试如何被标记为失败的,调用 `unwrap` 或 `expect` 都是非常有道理的。
|
||||
|
||||
### 当你比编译器知道更多的情况
|
||||
|
||||
当你有一些其他的逻辑来确保`Result`会是`Ok`值的时候调用`unwrap`也是合适的,虽然编译器无法理解这种逻辑。仍然会有一个`Result`值等着你处理:总的来说你调用的任何操作都有失败的可能性,即便在特定情况下逻辑上是不可能的。如果通过人工检查代码来确保永远也不会出现`Err`值,那么调用`unwrap`也是完全可以接受的,这里是一个例子:
|
||||
当你有一些其他的逻辑来确保 `Result` 会是 `Ok` 值的时候调用 `unwrap` 也是合适的,虽然编译器无法理解这种逻辑。仍然会有一个 `Result` 值等着你处理:总的来说你调用的任何操作都有失败的可能性,即便在特定情况下逻辑上是不可能的。如果通过人工检查代码来确保永远也不会出现 `Err` 值,那么调用 `unwrap` 也是完全可以接受的,这里是一个例子:
|
||||
|
||||
```rust
|
||||
use std::net::IpAddr;
|
||||
@ -26,29 +26,29 @@ use std::net::IpAddr;
|
||||
let home = "127.0.0.1".parse::<IpAddr>().unwrap();
|
||||
```
|
||||
|
||||
我们通过解析一个硬编码的字符来创建一个`IpAddr`实例。可以看出`127.0.0.1`是一个有效的 IP 地址,所以这里使用`unwrap`是没有问题的。然而,拥有一个硬编码的有效的字符串也不能改变`parse`方法的返回值类型:它仍然是一个`Result`值,而编译器仍然就好像还是有可能出现`Err`成员那样要求我们处理`Result`,因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就**确实**有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理`Result`了。
|
||||
我们通过解析一个硬编码的字符来创建一个 `IpAddr`实例。可以看出 `127.0.0.1` 是一个有效的 IP 地址,所以这里使用 `unwrap` 是没有问题的。然而,拥有一个硬编码的有效的字符串也不能改变 `parse` 方法的返回值类型:它仍然是一个 `Result` 值,而编译器仍然就好像还是有可能出现 `Err` 成员那样要求我们处理 `Result`,因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 **确实** 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 `Result` 了。
|
||||
|
||||
### 错误处理指导原则
|
||||
|
||||
在当有可能会导致有害状态的情况下建议使用`panic!`——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:
|
||||
在当有可能会导致有害状态的情况下建议使用 `panic!`——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:
|
||||
|
||||
* 有害状态并不包含**预期**会偶尔发生的错误
|
||||
* 有害状态并不包含 **预期** 会偶尔发生的错误
|
||||
* 之后的代码的运行依赖于不再处于这种有害状态
|
||||
* 当没有可行的手段来将有害状态信息编码进可用的类型中(并返回)的情况
|
||||
* 当没有可行的手段来将有害状态信息编码进所使用的类型中的情况
|
||||
|
||||
如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是`panic!`并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,`panic!`通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。
|
||||
如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 `panic!` 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,`panic!` 通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。
|
||||
|
||||
无论代码编写的多么好,当有害状态是预期会出现时,返回`Result`仍要比调用`panic!`更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回`Result`来表明失败预期是可能的,这样将有害状态向上传播,这样调用者就可以决定该如何处理这个问题。使用`panic!`来处理这些情况就不是最好的选择。
|
||||
无论代码编写的多么好,当有害状态是预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,这样调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。
|
||||
|
||||
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时`panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会`panic!`的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循**契约**(*contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这这通常代表调用方的 bug,而且这也不是那种你希望必须去处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的**程序员**需要修复他的代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
|
||||
当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这这通常代表调用方的 bug,而且这也不是那种你希望调用方必须处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的 **程序员** 需要修复他的代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。
|
||||
|
||||
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于`Option`的类型,而且程序期望它是**有值**的而不是**空值**。你的代码无需处理`Some`和`None`这两种情况,它只会有一种情况且绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像`u32`这样的无符号整型,也会确保它永远不为负。
|
||||
虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于 `Option` 的类型,而且程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some` 和 `None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。
|
||||
|
||||
### 创建自定义类型作为验证
|
||||
|
||||
让我们借用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型作为验证。回忆一下第二章的猜猜看游戏,它的代码请求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们事实上从未验证用户的猜测是位于这两个数字之间的,只保证它为正。在当前情况下,其影响并不是很严重:“Too high”或“Too low”的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字和输入字母时采取不同的行为。
|
||||
让我们借用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型作为验证。回忆一下第二章的猜猜看游戏,它的代码请求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们事实上从未验证用户的猜测是位于这两个数字之间的,只保证它为正。在当前情况下,其影响并不是很严重:“Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。
|
||||
|
||||
一种实现方式是将猜测解析成`i32`而不仅仅是`u32`,来默许输入负数,接着检查数字是否在范围内:
|
||||
一种实现方式是将猜测解析成 `i32` 而不仅仅是 `u32`,来默许输入负数,接着检查数字是否在范围内:
|
||||
|
||||
```rust,ignore
|
||||
loop {
|
||||
@ -69,14 +69,14 @@ loop {
|
||||
}
|
||||
```
|
||||
|
||||
`if`表达式检查了值是否超出范围,告诉用户出了什么问题,并调用`continue`开始下一次循环,请求另一个猜测。`if`表达式之后,就可以在知道`guess`在 1 到 100 之间的情况下与秘密数字作比较了。
|
||||
`if` 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用 `continue` 开始下一次循环,请求另一个猜测。`if` 表达式之后,就可以在知道 `guess` 在 1 到 100 之间的情况下与秘密数字作比较了。
|
||||
|
||||
然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。
|
||||
|
||||
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义`Guess`类型的方法,只有在`new`函数接收到 1 到 100 之间的值时才会创建`Guess`的实例:
|
||||
相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例:
|
||||
|
||||
```rust
|
||||
struct Guess {
|
||||
pub struct Guess {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ impl Guess {
|
||||
}
|
||||
|
||||
Guess {
|
||||
value: value,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,19 +97,18 @@ impl Guess {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 9-8: A `Guess` type that will only continue with
|
||||
values between 1 and 100</span>
|
||||
<span class="caption">列表 9-8:一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续</span>
|
||||
|
||||
首先,我们定义了一个包含`u32`类型字段`value`的结构体`Guess`。这里是储存猜测值的地方。
|
||||
首先,我们定义了一个包含 `u32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。
|
||||
|
||||
接着在`Guess`上实现了一个叫做`new`的关联函数来创建`Guess`的实例。`new`定义为接收一个`u32`类型的参数`value`并返回一个`Guess`。`new`函数中代码的测试确保了其值是在 1 到 100 之间的。如果`value`没有通过测试则调用`panic!`,这会警告调用这个函数的程序员有一个需要修改的 bug,因为创建一个`value`超出范围的`Guess`将会违反`Guess::new`所遵循的契约。`Guess::new`会出现 panic 的条件应该在其公有 API 文档中被提及;第十四章会涉及到在 API 文档中表明`panic!`可能性的相关规则。如果`value`通过了测试,我们新建一个`Guess`,其字段`value`将被设置为参数`value`的值,接着返回这个`Guess`。
|
||||
接着在 `Guess` 上实现了一个叫做 `new` 的关联函数来创建 `Guess` 的实例。`new` 定义为接收一个 `u32` 类型的参数 `value` 并返回一个 `Guess`。`new` 函数中代码的测试确保了其值是在 1 到 100 之间的。如果 `value` 没有通过测试则调用 `panic!`,这会警告调用这个函数的程序员有一个需要修改的 bug,因为创建一个 `value` 超出范围的 `Guess` 将会违反 `Guess::new` 所遵循的契约。`Guess::new` 会出现 panic 的条件应该在其公有 API 文档中被提及;第十四章会涉及到在 API 文档中表明 `panic!` 可能性的相关规则。如果 `value` 通过了测试,我们新建一个 `Guess`,其字段 `value` 将被设置为参数 `value` 的值,接着返回这个 `Guess`。
|
||||
|
||||
接着,我们实现了一个借用了`self`的方法`value`,它没有任何其他参数并返回一个`u32`。这类方法有时被称为 *getter*,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为`Guess`结构体的`value`字段是私有的。私有的字段`value`是很重要的,这样使用`Guess`结构体的代码将不允许直接设置`value`的值:调用者**必须**使用`Guess::new`方法来创建一个`Guess`的实例,这就确保了不会存在一个`value`没有通过`Guess::new`函数的条件检查的`Guess`。
|
||||
接着,我们实现了一个借用了 `self` 的方法 `value`,它没有任何其他参数并返回一个 `u32`。这类方法有时被称为 *getter*,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为 `Guess` 结构体的 `value` 字段是私有的。私有的字段 `value` 是很重要的,这样使用 `Guess` 结构体的代码将不允许直接设置 `value` 的值:调用者 **必须** 使用 `Guess::new` 方法来创建一个 `Guess` 的实例,这就确保了不会存在一个 `value` 没有通过 `Guess::new` 函数的条件检查的 `Guess`。
|
||||
|
||||
如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个`Guess`,而不是`u32`,同时其函数体中也无需进行任何额外的检查。
|
||||
如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个 `Guess`,而不是 `u32`,同时其函数体中也无需进行任何额外的检查。
|
||||
|
||||
## 总结
|
||||
|
||||
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!`宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的`Result`枚举代表操作可能会在一种可以恢复的情况下失败。可以使用`Result`来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用`panic!`和`Result`将会使你的代码在面对无处不在的错误时显得更加可靠。
|
||||
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!` 和 `Result` 将会使你的代码在面对无处不在的错误时显得更加可靠。
|
||||
|
||||
现在我们已经见识过了标准库中`Option`和`Result`泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如何在你的代码中利用他们。
|
||||
现在我们已经见识过了标准库中 `Option` 和 `Result` 泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如何在你的代码中利用他们。
|
@ -2,17 +2,17 @@
|
||||
|
||||
> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-00-generics.md)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit f65676e17d7fc4c0c7cd7275a7bf15447364831a
|
||||
|
||||
每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是**泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
|
||||
每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
|
||||
|
||||
同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像`i32`或`String`这样的具体值。我们已经使用过第六章的`Option<T>`,第八章的`Vec<T>`和`HashMap<K, V>`,以及第九章的`Result<T, E>`这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!
|
||||
同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32` 或 `String` 这样的具体值。我们已经使用过第六章的 `Option<T>`,第八章的 `Vec<T>` 和 `HashMap<K, V>`,以及第九章的 `Result<T, E>` 这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!
|
||||
|
||||
首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。
|
||||
|
||||
之后,我们讨论 *traits*,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
|
||||
之后,我们讨论 *trait*,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
|
||||
|
||||
最后介绍**生命周期**(*lifetimes*),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值同时仍然使编译器能够检查这些引用的有效性。
|
||||
最后介绍 **生命周期**(*lifetimes*),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。
|
||||
|
||||
## 提取函数来减少重复
|
||||
|
||||
@ -20,15 +20,15 @@
|
||||
|
||||
考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let numbers = vec![34, 50, 25, 100, 65];
|
||||
let number_list = vec![34, 50, 25, 100, 65];
|
||||
|
||||
let mut largest = numbers[0];
|
||||
let mut largest = number_list[0];
|
||||
|
||||
for number in numbers {
|
||||
for number in number_list {
|
||||
if number > largest {
|
||||
largest = number;
|
||||
}
|
||||
@ -39,22 +39,21 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-1: Code to find the largest number in a list
|
||||
of numbers</span>
|
||||
<span class="caption">列表 10-1:在一个数字列表中寻找最大值的函数</span>
|
||||
|
||||
这段代码获取一个整型列表,存放在变量`numbers`中。它将列表的第一项放入了变量`largest`中。接着遍历了列表中的所有数字,如果当前值大于`largest`中储存的值,将`largest`替换为这个值。如果当前值小于目前为止的最大值,`largest`保持不变。当列表中所有值都被考虑到之后,`largest`将会是最大值,在这里也就是 100。
|
||||
这段代码获取一个整型列表,存放在变量 `number_list` 中。它将列表的第一项放入了变量 `largest` 中。接着遍历了列表中的所有数字,如果当前值大于 `largest` 中储存的值,将 `largest` 替换为这个值。如果当前值小于目前为止的最大值,`largest` 保持不变。当列表中所有值都被考虑到之后,`largest` 将会是最大值,在这里也就是 100。
|
||||
|
||||
如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:
|
||||
如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码,这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let numbers = vec![34, 50, 25, 100, 65];
|
||||
let number_list = vec![34, 50, 25, 100, 65];
|
||||
|
||||
let mut largest = numbers[0];
|
||||
let mut largest = number_list[0];
|
||||
|
||||
for number in numbers {
|
||||
for number in number_list {
|
||||
if number > largest {
|
||||
largest = number;
|
||||
}
|
||||
@ -62,11 +61,11 @@ fn main() {
|
||||
|
||||
println!("The largest number is {}", largest);
|
||||
|
||||
let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8];
|
||||
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
|
||||
|
||||
let mut largest = numbers[0];
|
||||
let mut largest = number_list[0];
|
||||
|
||||
for number in numbers {
|
||||
for number in number_list {
|
||||
if number > largest {
|
||||
largest = number;
|
||||
}
|
||||
@ -76,8 +75,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-2: Code to find the largest number in *two*
|
||||
lists of numbers</span>
|
||||
<span class="caption">列表 10-2:寻找 **两个** 数字列表最大值的代码</span>
|
||||
|
||||
虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
|
||||
|
||||
@ -87,9 +85,9 @@ lists of numbers</span>
|
||||
|
||||
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。
|
||||
|
||||
在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做`largest`的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:
|
||||
在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做 `largest` 的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn largest(list: &[i32]) -> i32 {
|
||||
@ -105,24 +103,23 @@ fn largest(list: &[i32]) -> i32 {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let numbers = vec![34, 50, 25, 100, 65];
|
||||
let number_list = vec![34, 50, 25, 100, 65];
|
||||
|
||||
let result = largest(&numbers);
|
||||
let result = largest(&number_list);
|
||||
println!("The largest number is {}", result);
|
||||
# assert_eq!(result, 100);
|
||||
|
||||
let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8];
|
||||
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
|
||||
|
||||
let result = largest(&numbers);
|
||||
let result = largest(&number_list);
|
||||
println!("The largest number is {}", result);
|
||||
# assert_eq!(result, 6000);
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-3: Abstracted code to find the largest number
|
||||
in two lists</span>
|
||||
<span class="caption">列表 10-3:抽象后的寻找两个数字列表最大值的代码</span>
|
||||
|
||||
这个函数有一个参数`list`,它代表会传递给函数的任何具体`i32`值的 slice。函数定义中的`list`代表任何`&[i32]`。当调用`largest`函数时,其代码实际上运行于我们传递的特定值上。
|
||||
这个函数有一个参数 `list`,它代表会传递给函数的任何具体的 `i32`值的 slice。函数定义中的 `list` 代表任何 `&[i32]`。当调用 `largest` 函数时,其代码实际上运行于我们传递的特定值上。
|
||||
|
||||
从列表 10-2 到列表 10-3 中涉及的机制经历了如下几步:
|
||||
|
||||
@ -130,6 +127,6 @@ in two lists</span>
|
||||
2. 我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
|
||||
3. 我们将两个具体的存在重复代码的位置替换为了函数调用。
|
||||
|
||||
在不同的场景使用不同的方式泛型也可以利用相同的步骤来减少重复代码。与函数体中现在作用于一个抽象的`list`而不是具体值一样,使用泛型的代码也作用于抽象类型。支持泛型背后的概念与你已经了解的支持函数的概念是一样的,不过是实现方式不同。
|
||||
在不同的场景使用不同的方式,我们也可以利用相同的步骤和泛型来减少重复代码。与函数体中现在作用于一个抽象的 `list` 而不是具体值一样,使用泛型的代码也会作用于抽象类型。支持泛型背后的概念与你已经了解的支持函数的概念是一样的,不过是实现方式不同。
|
||||
|
||||
如果我们有两个函数,一个寻找一个`i32`值的 slice 中的最大项而另一个寻找`char`值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待!
|
||||
如果我们有两个函数,一个寻找一个 `i32` 值的 slice 中的最大项而另一个寻找 `char` 值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待!
|
@ -2,7 +2,7 @@
|
||||
|
||||
> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-01-syntax.md)
|
||||
> <br>
|
||||
> commit 55d9e75ffec92e922273c997026bb10613a76578
|
||||
> commit 56352c28cf3fe0402fa5a7cba73890e314d720eb
|
||||
|
||||
泛型用于通常我们放置类型的位置,比如函数签名或结构体,允许我们创建可以代替许多具体数据类型的结构体定义。让我们看看如何使用泛型定义函数、结构体、枚举和方法,并且在本部分的结尾我们会讨论泛型代码的性能。
|
||||
|
||||
@ -10,9 +10,10 @@
|
||||
|
||||
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
|
||||
|
||||
回到`largest`函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中`i32`最大值的函数。第二个函数寻找 slice 中`char`的最大值:
|
||||
回到 `largest` 函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。第二个函数寻找 slice 中 `char` 的最大值:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
fn largest_i32(list: &[i32]) -> i32 {
|
||||
@ -40,40 +41,39 @@ fn largest_char(list: &[char]) -> char {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let numbers = vec![34, 50, 25, 100, 65];
|
||||
let number_list = vec![34, 50, 25, 100, 65];
|
||||
|
||||
let result = largest_i32(&numbers);
|
||||
let result = largest_i32(&number_list);
|
||||
println!("The largest number is {}", result);
|
||||
# assert_eq!(result, 100);
|
||||
|
||||
let chars = vec!['y', 'm', 'a', 'q'];
|
||||
let char_list = vec!['y', 'm', 'a', 'q'];
|
||||
|
||||
let result = largest_char(&chars);
|
||||
let result = largest_char(&char_list);
|
||||
println!("The largest char is {}", result);
|
||||
# assert_eq!(result, 'y');
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-4: Two functions that differ only in their
|
||||
names and the types in their signatures</span>
|
||||
<span class="caption">列表 10-4:两个只在名称和签名中类型有所不同的函数</span>
|
||||
|
||||
这里`largest_i32`和`largest_char`有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!
|
||||
这里 `largest_i32` 和 `largest_char` 有着完全相同的函数体,所以如果能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!
|
||||
|
||||
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称`T`。任何标识符都可以作为类型参数名,选择`T`是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。`T`作为“type”的缩写是大部分 Rust 程序员的首选。
|
||||
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称 `T`。任何标识符都可以作为类型参数名,选择 `T` 是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。
|
||||
|
||||
当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。
|
||||
|
||||
我们将要定义的泛型版本的`largest`函数的签名看起来像这样:
|
||||
我们将要定义的泛型版本的 `largest` 函数的签名看起来像这样:
|
||||
|
||||
```rust,ignore
|
||||
fn largest<T>(list: &[T]) -> T {
|
||||
```
|
||||
|
||||
这可以理解为:函数`largest`有泛型类型`T`。它有一个参数`list`,它的类型是一个`T`值的 slice。`largest`函数将会返回一个与`T`相同类型的值。
|
||||
这可以理解为:函数 `largest` 有泛型类型 `T`。它有一个参数 `list`,它的类型是一个 `T` 值的 slice。`largest` 函数将会返回一个与 `T` 相同类型的值。
|
||||
|
||||
列表 10-5 展示一个在签名中使用了泛型的统一的`largest`函数定义,并向我们展示了如何对`i32`值的 slice 或`char`值的 slice 调用`largest`函数。注意这些代码还不能编译!
|
||||
列表 10-5 展示一个在签名中使用了泛型的统一的 `largest` 函数定义,并向我们展示了如何对 `i32` 值的 slice 或 `char` 值的 slice 调用 `largest` 函数。注意这些代码还不能编译!
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
fn largest<T>(list: &[T]) -> T {
|
||||
@ -89,25 +89,23 @@ fn largest<T>(list: &[T]) -> T {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let numbers = vec![34, 50, 25, 100, 65];
|
||||
let number_list = vec![34, 50, 25, 100, 65];
|
||||
|
||||
let result = largest(&numbers);
|
||||
let result = largest(&number_list);
|
||||
println!("The largest number is {}", result);
|
||||
|
||||
let chars = vec!['y', 'm', 'a', 'q'];
|
||||
let char_list = vec!['y', 'm', 'a', 'q'];
|
||||
|
||||
let result = largest(&chars);
|
||||
let result = largest(&char_list);
|
||||
println!("The largest char is {}", result);
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-5: A definition of the `largest` function that
|
||||
uses generic type parameters but doesn't compile yet</span>
|
||||
|
||||
<span class="caption">列表 10-5:一个还不能编译的使用泛型参数的 `largest` 函数定义</span>
|
||||
|
||||
如果现在就尝试编译这些代码,会出现如下错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error[E0369]: binary operation `>` cannot be applied to type `T`
|
||||
|
|
||||
5 | if item > largest {
|
||||
@ -116,7 +114,7 @@ error[E0369]: binary operation `>` cannot be applied to type `T`
|
||||
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
|
||||
```
|
||||
|
||||
注释中提到了`std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait,不过简单来说,这个错误表明`largest`的函数体不能适用于`T`的所有可能的类型;因为在函数体需要比较`T`类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的`std::cmp::PartialOrd` trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。
|
||||
注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait,不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型;因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。
|
||||
|
||||
<!-- Liz: this is the reason we had the topics in the order we did in the first
|
||||
draft of this chapter; it's hard to do anything interesting with generic types
|
||||
@ -128,9 +126,9 @@ what you think. /Carol -->
|
||||
|
||||
### 结构体定义中的泛型
|
||||
|
||||
同样也可以使用`<>`语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的`x`和`y`坐标值的结构体`Point`:
|
||||
同样也可以使用 `<>` 语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的 `x` 和 `y` 坐标值的结构体 `Point`:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
struct Point<T> {
|
||||
@ -144,14 +142,13 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-6: A `Point` struct that holds `x` and `y`
|
||||
values of type `T`</span>
|
||||
<span class="caption">列表 10-6:`Point` 结构体存放了两个 `T` 类型的值 `x` 和 `y`</span>
|
||||
|
||||
其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
|
||||
其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
|
||||
|
||||
注意`Point`的定义中只使用了一个泛型类型,我们想要表达的是结构体`Point`对于一些类型`T`是泛型的,而且字段`x`和`y`**都是**相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的`Point`的实例,像列表 10-7 中的代码就不能编译:
|
||||
注意 `Point` 的定义中只使用了一个泛型类型,我们想要表达的是结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x` 和 `y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像列表 10-7 中的代码就不能编译:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
struct Point<T> {
|
||||
@ -164,12 +161,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-7: The fields `x` and `y` must be the same
|
||||
type because both have the same generic data type `T`</span>
|
||||
<span class="caption">列表 10-7:字段 `x` 和 `y` 必须是相同类型,因为他们都有相同的泛型类型 `T`</span>
|
||||
|
||||
尝试编译会得到如下错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error[E0308]: mismatched types
|
||||
-->
|
||||
|
|
||||
@ -181,11 +177,11 @@ error[E0308]: mismatched types
|
||||
= note: found type `{float}`
|
||||
```
|
||||
|
||||
当我们将 5 赋值给`x`,编译器就知道这个`Point`实例的泛型类型`T`是一个整型。接着我们将`y`指定为 4.0,而它被定义为与`x`有着相同的类型,所以出现了类型不匹配的错误。
|
||||
当我们将 5 赋值给 `x`,编译器就知道这个 `Point` 实例的泛型类型 `T` 是一个整型。接着我们将 `y` 指定为 4.0,而它被定义为与 `x` 有着相同的类型,所以出现了类型不匹配的错误。
|
||||
|
||||
如果想要一个`x`和`y`可以有不同类型且仍然是泛型的`Point`结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改`Point`的定义为拥有两个泛型类型`T`和`U`。其中字段`x`是`T`类型的,而字段`y`是`U`类型的:
|
||||
如果想要定义一个 `x` 和 `y` 可以有不同类型且仍然是泛型的 `Point` 结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改 `Point` 的定义为拥有两个泛型类型 `T` 和 `U`。其中字段 `x` 是 `T` 类型的,而字段 `y` 是 `U` 类型的:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
struct Point<T, U> {
|
||||
@ -200,14 +196,13 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-8: A `Point` generic over two types so that
|
||||
`x` and `y` may be values of different types</span>
|
||||
<span class="caption">列表 10-8:使用两个泛型的 `Point`,这样 `x` 和 `y` 可能是不同类型</span>
|
||||
|
||||
现在所有这些`Point`实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
|
||||
现在所有这些 `Point` 实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
|
||||
|
||||
### 枚举定义中的泛型数据类型
|
||||
|
||||
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的`Option<T>`枚举,现在这个定义看起来就更容易理解了。让我们再看看:
|
||||
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 `Option<T>` 枚举,现在这个定义看起来就更容易理解了。让我们再看看:
|
||||
|
||||
```rust
|
||||
enum Option<T> {
|
||||
@ -216,9 +211,9 @@ enum Option<T> {
|
||||
}
|
||||
```
|
||||
|
||||
换句话说`Option<T>`是一个拥有泛型`T`的枚举。它有两个成员:`Some`,它存放了一个类型`T`的值,和不存在任何值的`None`。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值”是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。
|
||||
换句话说 `Option<T>` 是一个拥有泛型 `T` 的枚举。它有两个成员:`Some`,它存放了一个类型 `T` 的值,和不存在任何值的`None`。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值” 是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。
|
||||
|
||||
枚举也可以拥有多个泛型类型。第九章使用过的`Result`枚举定义就是一个这样的例子:
|
||||
枚举也可以拥有多个泛型类型。第九章使用过的 `Result` 枚举定义就是一个这样的例子:
|
||||
|
||||
```rust
|
||||
enum Result<T, E> {
|
||||
@ -227,15 +222,15 @@ enum Result<T, E> {
|
||||
}
|
||||
```
|
||||
|
||||
`Result`枚举有两个泛型类型,`T`和`E`。`Result`有两个成员:`Ok`,它存放一个类型`T`的值,而`Err`则存放一个类型`E`的值。这个定义使得`Result`枚举能很方便的表达任何可能成功(返回`T`类型的值)也可能失败(返回`E`类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景,当文件被成功打开`T`被放入了`std::fs::File`类型而当打开文件出现问题时`E`被放入了`std::io::Error`类型。
|
||||
`Result` 枚举有两个泛型类型,`T` 和 `E`。`Result` 有两个成员:`Ok`,它存放一个类型 `T` 的值,而 `Err` 则存放一个类型 `E` 的值。这个定义使得 `Result` 枚举能很方便的表达任何可能成功(返回 `T` 类型的值)也可能失败(返回 `E` 类型的值)的操作。回忆一下列表 9-2 中打开一个文件的场景:当文件被成功打开 `T` 被放入了 `std::fs::File` 类型而当打开文件出现问题时 `E` 被放入了 `std::io::Error` 类型。
|
||||
|
||||
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。
|
||||
|
||||
### 方法定义中的枚举数据类型
|
||||
|
||||
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体`Point<T>`。接着我们在`Point<T>`上定义了一个叫做`x`的方法来返回字段`x`中数据的引用:
|
||||
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体 `Point<T>`。接着我们在 `Point<T>` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
struct Point<T> {
|
||||
@ -256,11 +251,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 10-9: Implementing a method named `x` on the
|
||||
`Point<T>` struct that will return a reference to the `x` field, which is of
|
||||
type `T`.</span>
|
||||
<span class="caption">列表 10-9:在 `Point<T>` 结构体上实现方法 `x`,它返回 `T` 类型的字段 `x` 的引用</span>
|
||||
|
||||
注意必须在`impl`后面声明`T`,这样就可以在`Point<T>`上实现的方法中使用它了。
|
||||
注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point<T>` 上实现的方法中使用它了。
|
||||
|
||||
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体`Point<T, U>`上定义了一个方法`mixup`。这个方法获取另一个`Point`作为参数,而它可能与调用`mixup`的`self`是不同的`Point`类型。这个方法用`self`的`Point`类型的`x`值(类型`T`)和参数的`Point`类型的`y`值(类型`W`)来创建一个新`Point`类型的实例:
|
||||
|
||||
|
@ -404,7 +404,7 @@ fn first_word<'a>(s: &'a str) -> &'a str {
|
||||
|
||||
现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,而后两条规则则适用于输出生命周期。如果编译器检查完这三条规则并仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。
|
||||
|
||||
1. 每一个是引用的参数都有它自己的生命周期参数。话句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
|
||||
1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。
|
||||
|
||||
2. 如果只有一个输入生命周期参数,那么它被赋给所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
|
||||
|
||||
@ -529,7 +529,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st
|
||||
}
|
||||
```
|
||||
|
||||
这个是列表 10-21 中那个返回两个字符串 slice 中最长者的`longest`函数,不过带有一个额外的参数`ann`。`ann`的类型是泛型`T`,它可以被放入任何实现了`where`从句中指定的`Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么`Display` trait bound 是必须的。因为生命周期也是泛型,生命周期参数`'a`和泛型类型参数`T`都位于函数名后的同一尖括号列表中。
|
||||
这个是列表 10-21 中那个返回两个字符串 slice 中最长者的`longest`函数,不过带有一个额外的参数`ann`。`ann`的类型是泛型`T`,它可以被放入任何实现了`where`从句中指定的`Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么`Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数`'a`和泛型类型参数`T`都位于函数名后的同一尖括号列表中。
|
||||
|
||||
## 总结
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user