# 错误处理 在软件中,错误是家常便饭,因此 Rust 有数个用于处理某些东西出了岔子情形的特性。在许多情况下,Rust 都要求编程者知晓某种错误的可能性,进而在代码编译之前就采取一些措施。这样的要求通过确保编程者在将其代码部署到生产环境之前,会发现错误并对其进行恰当处理,而使得他们的程序更为健壮! Rust 将错误分组为两个主要类别: *可恢复(recoverable)* 与 *不可恢复(unrecoverable)* 错误。对于可恢复错误,比如 *文件未找到* 错误,大多数情况下只要将该故障汇报给用户,并重试该操作。而不可恢复错误则总是代码错误的表征,像是尝试访问超出数组末端的某个位置,进而因此就要立即停止该程序。 大多数语言都没有区分这两种错误,而以同样方式,使用诸如异常的机制处理这两种错误。Rust 没有异常。相反,Rust 有着用于可恢复错误的类型 `Result`,以及在程序发生了不可恢复错误时,停止程序执行的 `panic!` 宏(the `panic!` macro)。本章将首先涵盖对 `panic!` 的调用,并在随后讲解那些返回的 `Result` 值。此外,这里会对在决定是否要尝试从错误中恢复,还是要停止程序的执行时的诸多考虑,进行探讨。 ## 带 `panic!` 的不可恢复错误 **Unrecoverable Errors with `panic!`** 某些时候,在代码中不好的事情发生了,而对其无计可施。在这些情形下,Rust 有着 `panic!` 宏。在 `panic!` 宏执行时,程序就会打印一条失败消息,释放(unwind)并清理掉栈,并在随后退出。在侦测到某种类别的代码错误,且在编写程序时刻,尚不清楚怎样处理这个故障时,就会触发一个程序中止(invoke a panic)。 > **对程序终止进行响应的栈解除或栈终止(Unwinding the Stack or Aborting in Response to a Panic)** > > 默认情况下,在程序终止发生时,程序就开始 *解除栈(unwinding)*,这是指 Rust 对栈进行回退,并清理他所遇到的各个函数的数据。然而,这样的回退与清理,是很多的工作量。那么因此 Rust 就允许编程者选择立即 *终止(aborting)* 的替代方案,该替代方案会不加清理的结束程序。程序曾用过的内存,这时就需要由操作系统来清理。如过在项目中,需要将生成的二进制执行文件构造得尽可能小,你们就可以通过把 `panic= 'abort'`,添加到 `Cargo.toml` 文件中恰当的 `[profile]` 小节,而从程序中止的栈解除切换为立即终止。比如,若想要在发布模式中,于程序中止时立即终止,那么就要添加这个: ```toml [profile.release] panic = 'abort' ``` 下面就来在一个简单程序中,尝试调用 `panic!` 宏: 文件名:`src/main.rs` ```rust fn main() { panic! ("崩溃并燃烧"); } ``` 在运行该程序时,就会看到下面这样的东西: ```console $ cargo run Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo) Finished dev [unoptimized + debuginfo] target(s) in 0.40s Running `target/debug/error_handling_demo` thread 'main' panicked at '崩溃并燃烧', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 这个到 `panic!` 的调用,引起了包含在最后两行中的错误消息。第一行给出了程序中止消息及源码里程序中止发生的位置:`src/main.rs:2:5` 表示是在这里的 `src/main.rs` 文件的第二行、第五个字符处。 在此情况下,所指出的那行,就是这里代码的一部分,而在前往到那行时,就会看到那个 `panic!` 宏调用。在别的情况下,`panic!` 调用可能会在所编写代码调用的代码中,那么由该错误消息报告出的文件名与行号,就会是 `panic!` 被调用所在之处的其他人的代码,而不会是最终引起那个 `panic!` 调用的自己编写的代码行。这里可以使用该 `panic!` 调用来自那些函数的回溯,来弄清楚此处代码的哪个部分导致了该问题。接下来就要详细讨论这种回溯。 ### 运用 `panic!` 回溯 现在来看一下另一个示例,看看由于代码中的编码错误,而非由于在代码中直接调用 `panic!` 宏时,来自库的 `panic!` 调用到底会是什么样子。下面清单 9-1 有一些尝试访问某个矢量中超出了有效索引范围索引的代码。 文件名:`src/main.rs` ```rust fn main() { let v = vec! [1, 2, 3]; v[99]; } ``` *清单 9-1:尝试访问某个超出了矢量末端的元素,这会导致一个到 `panic!` 的调用* 这里正尝试访问这里矢量的第 100 个元素(由于索引开始于零处,故那是在索引 99 处),但这个矢量只有 3 个元素。在此情况下,Rust 就会中止。使用 `[]` 被认为是要返回一个元素的,但在传递某个无效索引时,这里就没有 Rust 可返回的正确元素。 在 C 语言中,尝试读取超出某种数据结构,属于未定义的行为。那么就可以会得到与该数据结构中元素对应的、内存中那个位置处的任何东西,即便该内存不属于那个数据结构。这就叫做 *缓冲区重读取(a buffer overread)*,并能在攻击者可以这样的方式操作索引,来读取存储在该数据结构之后的、本不应允许他们读取的数据时,导致安全漏洞。 Rust 为保护程序免受这类漏洞的危害,就会在尝试位于某个不存在索引处的元素时,停止程序的执行而拒绝继续下去。来尝试运行一下上面的代码看看: ```console $ cargo run lenny@vm-manjaro Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo) Finished dev [unoptimized + debuginfo] target(s) in 0.47s Running `target/debug/error_handling_demo` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 此错误指出了,在这里 `main.rs` 的第 4 行,其中尝试访问索引 `99` 处。接下来的注解行,讲到这里可将 `RUST_BACKTRACE` 环境变量,设置为获取究竟发生什么,才导致了这个错误。所谓 *回溯(a backtrace)*,即为到此已调用的全部函数的清单。Rust 中的回溯,与其他语言中的回溯完成的事情一样:阅读回溯的冠军,就是要从顶部开始,一直要读到自己编写的文件为止。那便是该问题缘起之处。在那个点位之上的那些行,就是所编写代码曾调用过的代码;而所编写代码之下的那些行,则是调用所编写代码的代码。这些前前后后的行,就可能包含核心 Rust 代码、 标准库代码,或者正使用着的代码箱。下面就来通过将 `RUST_BACKTRACE` 环境变量,设置为除 `0` 之外的任何值,尝试获取到回溯。下面清单 9-2 展示了与将会看到的类似输出。 ```console $ RUST_BACKTRACE=1 cargo run lenny@vm-manjaro Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/error_handling_demo` 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: rust_begin_unwind at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5 1: core::panicking::panic_fmt at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14 2: core::panicking::panic_bounds_check at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5 3: >::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10 4: core::slice::index:: for [T]>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9 5: as core::ops::index::Index>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9 6: error_handling_demo::main at ./src/main.rs:4:5 7: core::ops::function::FnOnce::call_once at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. ``` *清单 9-2:在设置了 `RUST_BACKTRACE` 环境变量时,由到 `panic!` 调用生成的回溯被显示了出来* 那可是很多的输出了!具体看到的输出,可能根据操作系统与 Rust 版本而有所不同。为从此信息中获得回溯,就要开启那些调试符号。在不带 `--release` 标志使用 `cargo build` 或 `cargo run` 时,如同这里这样,这些调试符号默认就是开启的。 在上面清单 9-2 里的输出中,回溯所指向到这里项目中行的第 6 行,就是导致问题的行:即 `src/main.rs` 的第 4 行。在不想要这个程序中止时,就应在首个提到了自己所编写文件的行,所指向的那个位置,开始排查。在之前的清单 9-1 中,那里有意编写了会中止的代码,而修正程序中止的办法,就是不要请求某个超出那个矢量索引范围的元素。而在今后代码中止时,就需要搞清楚代码是在对什么值进行何种操作,而导致了中止,以及代码应该怎么做。 在本章的 [要 `panic!` 或不要 `panic!`](#to-panic-or-not-to-panic) 小节,将回到 `panic!` 这个话题,并讨论在何时要用 `panic!`,何时不应使用 `panic!` 来处理不同错误情形。接下来,就会看看怎样使用 `Result`,从错误中恢复过来。 ## 带有 `Result` 的可恢复错误 多数错误都没有严重到要求程序整个地停止运行。某些时候,在某个函数失败时,必定是由于某种可易于解释进而加以响应的原因。比如在尝试打开某个文件,而因为要打开的文件不存在,那个操作失败了时,那么可能希望创建该文件,而不是中止这个进程。 回顾第二章中的 [处理潜在带有 `Result` 类型的程序失败](Ch02_Programming_a_Guessing_Game.md#handling-potential-failure-with-the-result-type) 小节,其中的 `Result` 枚举被定义为有两个变种,`Ok` 与 `Err`,如下所示: ```rust enum Result { Ok, Err, } ``` 这里的 `T` 与 `E`,都属于泛型参数(generic type parameters):在第 10 章就会更深入讨论泛型。此刻需要明白的是,这里的 `T` 表示在操作成功情形下,那个 `Ok` 变种里返回值的类型,而这里的 `E`,则表示在失效情形下,将返回的在 `Err` 变种里错误的类型。由于 `Result` 有着这些泛型参数,因此就可以在打算返回成功值与错误值有所区别的许多不同情形下,使用到这个 `Result` 及定义在其上的函数。 下面就来调用一个由于其会失败,而返回 `Result` 值的函数。在下面清单 9-3 中,是尝试打开一个文件。 文件名:`src/main.rs` ```rust use std::fs::File; fn main() { let f = File::open("hello.txt"); } ``` *清单 9-3:打开某个文件* 怎样知道 `File::open` 会返回一个 `Result` 呢?这里就可以看看 [标准库 API 文档](https://doc.rust-lang.org/std/fs/struct.File.html#method.open),或者可以询问一下编译器!在赋予 `f` 一个明知 *不是* 该函数返回值类型的类型注解,并随后尝试编译该代码时,编译器就会告知,这两个类型不匹配。给出的错误消息,就会告诉 `f` 的类型是什么。来试试吧!这里已知 `File::open` 的返回类型不是 `u32`,因此就把那个 `let f` 语句修改为下面这样: ```rust let f: u32 = File::open("hello.txt"); ``` 现在尝试编译,就会给到接下来的输出: ```console $ cargo run lennyp@vm-manjaro Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo) error[E0308]: mismatched types --> src/main.rs:4:18 | 4 | let f: u32 = File::open("hello.txt"); | --- ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result` | | | expected due to this | = note: expected type `u32` found enum `Result` For more information about this error, try `rustc --explain E0308`. error: could not compile `error_handling_demo` due to previous error ``` 这就是说,`File::open` 函数的返回类型,是个 `Result`。泛型参数 `T`,在这里已被使用成功值的类型,`std::fs::File`,即一个文件句柄(a file handle)填充。而用于错误值的类型 `E`,则为 `std::io::Error`。 这样的返回值类型,表示到 `File::open` 的调用,可能会成功而返回一个能够自该处读取,或写入到该处的文件句柄。该函数调用同样可能失败:比如该文件可能不存在,或可能没有访问该文件的权限。那么这个 `File::open` 函数,就需要具备已知告知其是否成功或失败的方式,与此同时给到一个文件句柄,或者错误信息。这样的信息,正是这个 `Result` 枚举所要表达的。 此示例中,在 `File::open` 成功处,变量 `f` 中的值就会是包含了一个文件句柄的 一个 `Ok` 实例。而在其失败的情况下,`f` 中的那个值,就会是包含了有关所发生错误类别的更多信息的一个 `Err` 实例。 这里就需要对清单 9-3 中代码进行添加,从而根据 `File::open` 所返回值,而采取不同措施。下面清单 9-4 就给出了一种使用基本工具,即在第 6 章中曾讨论过的 `match` 表达式,对那个 `Result` 进行处理的方法。 文件名:`src/main.rs` ```rust use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(e) => panic! ("打开文件出现问题:{:?}", e), }; } ``` *清单 9-4:运用 `match` 表达式来处理可能返回的各个 `Result` 变种* 请注意,与 `Option` 枚举类似,这个 `Result` 枚举及其变种,是已由 Rust 前奏(the prelude)带入到作用域中了的,因此这里无需在那两个 `match` 支臂中的 `Ok` 与 `Err` 变种之前,指明 `Result::`。 在返回结果为 `Ok` 时,此代码就会返回从 `Ok` 变种抽出的那个内部的 `file` 值,且这里随后就把那个文件句柄值,指派给那个变量 `f`。在这个 `match` 之后,就可以将这个文件句柄,用于读取或写入了。 而那个 `match` 的另一支臂,则处理了从 `File::open` 得到一个 `Err` 值的情形。在此示例中,选择了调用 `panic!` 宏。在当前目录中没有名为 `hello.txt` 的文件,并运行此代码时,就会看到来自那个 `panic!` 宏的如下输出: ```console $ cargo run lennyp@vm-manjaro Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/error_handling_demo` thread 'main' panicked at '打开文件出现问题:Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:19 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 与往常一样,此输出告知了到底什么出错了。 ### 匹配各异的错误 上面清单 9-4 中的代码,不论 `File::open` 因何而失败,都会 `panic!`。然而,这里是要因应不同失败原因,而采取不同措施:在 `File::open` 因为那个文件不存在而失败时,就要创建该文件并返回到那个新建文件的句柄。在那个 `File::open` 因别的其他原因失败 -- 比如没有打开该文件的权限时,这里仍要该代码以清单 9-4 中所做的同样方式,`panic!` 掉。为此,这里就要添加一个内部的 `match` 表达式,如下清单 9-5 中所示。 文件名:`src/main.rs` ```rust use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(e) => match e.kind() { ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(error) => panic! ("创建该文件时出现问题:{:?}", error), }, other_error => panic! ("打开文件出现问题:{:?}", other_error), }, }; } ``` *清单 9-5:以不同方式处理不同类别的错误* `File::open` 所返回的位于 `Err` 变种内部的值的类型为 `io::Error`,他是一个由标准库提供的结构体。该结构体有个可供调用以获取到 `io::ErrorKind` 值的方法 `kind`。而枚举 `io::ErrorKind` 亦是由标准库提供,并有着表示那些可能自某个 `io` 操作而引起的,不同类别错误的一些变种。这里打算使用的变种为 `ErrorKind::NotFound`,表示了正尝试打开的文件尚不存在。因此这里既对 `f` 进行了匹配,而同时还有了在 `e.kind()` 上的一个内层匹配。 这里打算检查的那个内层匹配中的条件,则是由 `e.king()` 所返回的那个值,是否为 `ErrorKind` 枚举的 `NotFound` 变种。在 `e.kind()` 返回的值为 `ErrorKind` 的 `NotFound` 变种时,这里就尝试以 `File::create` 来创建该文件。然而由于 `Fiel::create` 仍会失败,因此这里就需要在那个内层 `match` 表达式中的第二个支臂。在该文件无法被创建出来时,就会打印出一条不同的错误消息。外层那个 `match` 表达式的第二支臂保持原样,因此该程序会在除了文件未找到错误之外的其他任何错误时,都会中止运行。 > **这种结合`Result` 运用 `match` 表达式的替代方案** > > 那可是有好多的 `match` !`match` 表达式是很有用,但同样也是很原始的。在第 13 章,就会了解到闭包(closures),这种与定义在 `Result` 上的众多方法一起使用的特性。在对代码中的 `Result` 值进行处理时,比起使用 `match` 表达式,这样的闭包方式可以简练得多。 > 比如,下面就是编写与清单 9-5 中同样逻辑,不过却使用了闭包特性与 `unwrap_or_else` 方法的另一种方式。 ```rust use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt").unwrap_or_else(|e| { if e.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic! ("创建文件时发生问题:{:?}", error); }) } else { panic! ("打开文件时出现问题:{:?}", e); } }); println! ("{:?}", f); } ``` > 尽管此代码与清单 9-5 有着同样行为,但他并未包含任何的 `match` 表达式,且读起来更清楚。请在读完了第 13 章,并看看标准库文档中的这个 `unwrap_or_else` 方法后,再回到这个示例。在对错误进行处理时,许多别的这些方法,都可以清理掉大量嵌套的 `match` 表达式。 ### 因错误而中止的快捷方式:`unwrap` 与 `expect` **Shortcuts for Panic on Error: `unwrap` and `expect`** 运用 `match` 运作足够良好,不过那样可能有点冗长,且不总是良好地传达了意图。这个 `Result` 类型,其上本来就定义了许多用于完成各种各样的、更为具体任务的辅助方法。其中的 `unwrap` 方法,就是一个实现了刚好与前面清单 9-4 中所编写的 `match` 表达式类似的快捷方法。在 `Result` 的值为 `Ok` 变种时,`unwrap` 就会返回那个 `Ok` 内部的值。而在该 `Result` 为 `Err` 变种时,`unwrap` 则会代为调用 `panic!` 宏。下面就是运作中的一个 `unwrap` 示例: 文件名:`src/main.rs` ```rust use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); } ``` 在没有 `hello.txt` 文件下运行此程序时,就会看到一条来自由这个 `unwrap` 方法做出的 `panic!` 宏调用的错误消息: ```console $ cargo run lennyp@vm-manjaro Finished dev [unoptimized + debuginfo] target(s) in 0.03s Running `target/debug/error_handling_demo` thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 同样,`Result` 上的 `expect` 方法,则可实现对这条 `panic!` 错误消息的选取。使用 `expect` 而非 `unwrap` 并提供良好的错误消息,就能够传达到自己的意图,进而令到追踪程序中止缘由更为容易。`expect` 方法的语法如下所示: 文件名:`src/main.rs` ```rust use std::fs::File; fn main() { let f = File::open("hello.txt").expect("打开 hello.txt 失败"); } ``` 这里以与 `unwrap` 同样方式,使用了 `expect`:用于返回文件句柄,或者对 `panic!` 宏进行调用。而在 `expect` 调用 `panic!` 时用到的错误消息,就将是这里传递给 `expect` 的那个参数,而不再是 `unwrap` 所用到的那个默认 `panic!` 消息了。下面就是该错误消息看起来的样子: ```console $ cargo run lennyp@vm-manjaro Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo) Finished dev [unoptimized + debuginfo] target(s) in 1.23s Running `target/debug/error_handling_demo` thread 'main' panicked at '打开 hello.txt 失败: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 由于此错误消息是以这里所指定的,`打开 hello.txt 失败` 开始,因此就会更易于搞清楚,此错误消息来自代码中的何处。而若在多处使用 `unwrap`,那么在要精准找出到底是那个 `unwrap` 导致了程序中止时,就会因为所有这些调用了 `panic!` 的 `unwrap`,都打印出同样消息,而要耗费更多时间。 ### 传递错误(Propagating Errors) 在某函数实现调用了可能失败的某些东西时,与其在该函数自身里头对错误进行处理,还可以将该错误返回给调用该函数的代码,这样调用该函数的代码就可以自己决定要做些什么。这就叫做 *传递(propagating)* 错误,而将更多的控制,给到调用该函数的代码,相比于当前实现的函数代码,调用代码中可能会有更多决定该错误应如何被处理的信息或逻辑。 比如,下面清单 9-6 就给出了一个从某个文件读取用户名的函数。在那个文件不存在或无法读取时,这个函数就会将这些错误返回给调用该函数的代码。 ```rust use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } ``` *清单 9-6:使用 `match` 将错误返回给调用代码的一个函数* 虽然可以简单得多的方式,来重写该函数,不过为了对错误处理进行探索,因此这里就要通过亲自动手完成其大部分代码开头;在结束时,就会给出那更简短的方式。首先来看看该函数的返回值类型:`Result`。这表示该函数要返回一个类型 `Result` 的值,其中的泛型参数 `T` 已被具体类型 `String` 填充,而那个泛型 `E` 则已被具体类型 `io::Error` 填充。若此函数不带任何问题的成功运行,那么调用该函数的代码,就会收到一个保存着一个 `String` 的 `Ok` 值 -- 即该函数从那个文件中读取到的用户名。而在该函数出现任何问题时,那么调用代码就会收到一个,保存着包含了有关所出现问题更多信息的 `io::Error` 示例的 `Err` 值。这里之所以选择 `io::Error` 作为此函数的返回值,是因为在该函数的函数体中所调用的两个都可能失败的操作:`File::open` 与 `read_to_string`,他们所返回的错误值都是这个 `io::Error` 类型。 该函数的函数体,是以调用 `File::open` 函数开始的。随后这里就以与清单 9-4 中类似方式,使用了一个 `match` 处理 `File::open` 返回的 `Result`。在 `File::open` 成功时,那么在模式变量 `file` 中的文件句柄,就成为那个可变变量 `f` 中的值,且函数会继续执行。而在 `Err` 情形下,这里使用了 `return` 关键字,早早地就从这个函数 `return` 了出去,同时将来自 `File::open` 的那个错位值,此时是在模式变量 `e` 中,作为该函数的错误值,传回给调用该函数的代码。 因此在 `username_file` 有着一个文件句柄时,该函数随后就会创建一个在变量 `username` 中的新 `String`,并调用 `username_file` 中文件句柄上的 `read_to_string` 方法,来将该文件中的内容,读取到 `username` 中。因为即使 `File::open` 运行成功,这个 `read_to_string` 仍可能失败,因此他同样会返回一个 `Result`。那么这里就需要另一个 `match`,来处理这个 `Result`:在 `read_to_string` 成功时,那么接下来这个函数就成功执行了,进而就从这个文件,返回到此时位于封装在一个 `Ok` 中的 `username` 中的用户名来。而在 `read_to_string` 失败时,这里就会以与之前在那个处理 `File::open` 返回值的 `match` 中返回错误值的同样方式,返回现在这个 `read_to_string` 的错误值。不过,由于这是该函数中的最后一个表达式,因此这里无需显示地写下 `return`。 调用此代码的代码,随后就会对收到的包含了用户名 `Ok` 值,或者包含了一个 `io::Error` 类型的 `Err` 值进行处理。至于要对这些值做何处理,则取决于调用代码了。在调用代码收到 `Err` 值时,他就可以采取好比调用 `panic!` 并崩溃掉该程序,可以使用某个默认用户名,或者从相比该文件的其他地方,查找该用户名等操作。这里没有关于那个调用代码确切地尝试要做什么的足够信息,因此这里就把全部的成功或错误信息,向上传递给调用代码,让调用代码进行适当处理。 由于在 Rust 中这样的传递错误模式是如此普遍,以致于 Rust 提供了问好操作符(the question mark operator, `?`),来令到错误传递更加容易。 ### 传递错误的快捷方式:`?` 操作符 下面清单 9-7 给出了与清单 9-6 有着同样功能的一个 `read_username_from_file` 实现,只是此实现使用了 `?` 操作符。 文件名:`src/main.rs` ```rust use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } ``` *清单 9-7:一个使用 `?` 操作符将错误返回给调用代码的函数* 那个放在某个 `Result` 值后面的 `?`,被定义为几乎与之前所定义的那些,用于处理清单 9-5 中那些 `Result` 值的 `match` 表达式,以同样方式运作。在 `Result` 的值为 `Ok` 时,那么那个 `Ok` 内部的值,就会从该表达式得以返回,且程序将继续运行。而在该 `Result` 值为一个 `Err` 时,则会如同之前曾用到的 `return` 关键字一样,将自这整个函数,返回这个 `Err` 值,进而这个错误值,就被传递给了调用代码。 清单 9-6 中的 `match` 表达式完成的事情,与这个 `?` 操作符完成的事情有个不同点:调用了这个 `?` 操作符的错误值,会经过定义在标准库中 `From` 特质(the `From` trait in the standard library)中定义的 `from` 函数,而该函数被用于将一种类型的值,转换到另一种类型中。当 `?` 操作符调用 `from` 函数时,被接收到的错误类型,就被转换为了定义在当前函数返回值类型中的类型了(即 `Result`)。在某个函数可能失败,即便该函数的一些部分而不是整个函数,由于许多不同原因而失败,而返回一种表示这些全部失败方式的一种错误类型时,这个不同之处就会有用。 比如,这里本可将清单 9-7 中的 `read_username_from_file` 函数,修改为返回一个自己定义的名为 `OurError` 的定制错误类型。而在同时给 `OurError` 定义了 `impl From`,以从 `io::Error` 构造出一个 `OurError` 的实例时,那么随后无需添加任何代码到这个函数,`read_username_from_file` 函数中的这些 `?` 操作符,就会调用 `from` 并对那些错误类型进行转换。 在清单 9-7 的语境下,位于 `File::open` 调用末尾的那个 `?`,将返回一个 `Ok` 内部的值给变量 `username_file`。而在有错误发生时,这个 `?` 操作符,就会早早地从整个函数退出,并把任何的 `Err` 值给到调用代码。对于那个 `read_to_string` 调用末尾处的 `?`,适用这同样的情况。 `?` 操作符消除了很多样板代码(a lot of boilerplate),并令到此函数的实现更为简单。通过将这些方法调用在整个 `?` 即刻链接起来,甚至可以进一步缩短此代码,如下清单 9-8 中所示。 文件名:`src/main.rs` ```rust use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } ``` *清单 9-8:在 `?` 操作符后将方法调用链接起来* 这里已将那个 `username` 中的新 `String` 的创建,挪到了该函数的开头;整个函数就整个部分未作改动。这里没有了变量 `username_file` 的创建,而是已将到 `read_to_string` 的函数调用,直接链接到了 `File::open("hello.txt")?` 的结果上。在 `read_to_string` 调用的末尾仍有一个 `?`,同时在这两个 `File::open` 与 `read_to_string` 调用都成功,而不返回错误时,这里就会返回一个包含了 `username` 的 `Ok` 值。功能仍旧与清单 9-6 和清单 9-7 中是一样的;这只是一种不同的、更为符合人体工程学的编写方式。 下面清单 9-9 给出了使用 `fs::read_to_string` 的一种甚至更加简短的方式。 文件名:`src/main.rs` ```rust use std::fs; use std::io; fn read_username_from_file() -> Result { fs::read_to_string("hello.txt") } ``` *清单 9-9:使用 `fs::read_to_string` 而非打开在读取那个文件* 将某个文件读取到字符串中,是个相当常见的操作,因此标准库提供了便捷的打开文件、创建一个新 `String`、读取文件内容、将内容放入到那个 `String`,并将其返回的 `fs::read_to_string` 函数。当然,`fs::read_to_string` 的使用,并不能赋予到这里对全部错误处理加以解释的机会,因此这里才要走过前面那些常常的过程。 ### 哪些地方可以使用 `?` 操作符 `?` 操作符仅可用于那些返回值类型,与这个 `?` 被用于的那个值类型兼容的函数中。这是由于 `?` 操作符被定义为与在清单 9-6 中,所定义的那个 `match` 表达式类似方式,执行一个该函数早期阶段的退出。在清单 9-6 中,那个 `match` 使用的是一个 `Result` 值,同时那个先期返回支臂返回的是一个 `Err(e)` 值。那么那个函数的返回值类型,就必须是个 `Result`,这样才与这个 `return` 兼容。 在下面清单 9-10 中,就要看看一个有着与其上使用了 `?` 的类型值不兼容返回值的 `main` 函数中,使用 `?` 操作符会收到的错误: 文件名:`src/mian.rs` ```rust use std::fs::File; fn main() { let greating_file = File::open("hello.txt"); } ``` *清单 9-10:尝试在返回 `()` 的 `main` 函数中使用 `?` 就不会编译* 此代码是要打开一个文件,这就可能失败。那个 `?` 操作符接续了由 `File::open` 所返回的 `Return` 值,然而这个 `main` 函数的返回值类型为 `()`,而非 `Result`。那么在编译此代码时,就会得到以下的错误消息: ```console $ cargo run lennyp@vm-manjaro Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo) error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:48 | 3 | / fn main() { 4 | | let greating_file = File::open("hello.txt")?; | | ^ cannot use the `?` operator in a function that returns `()` 5 | | } | |_- this function should return `Result` or `Option` to accept `?` | = help: the trait `FromResidual>` is not implemented for `()` For more information about this error, try `rustc --explain E0277`. error: could not compile `error_handling_demo` due to previous error ``` 此错误指出了只允许在返回 `Result`、`Option` 或别的实现了 `FromResidual` 的类型的函数中,使用 `?` 操作符。 而要修正这个错误,则有两个选择。一个选择是在没有修改函数返回值类型的限制时,那么就将其修改为与在其上使用 `?` 操作符的值类型兼容。另一技巧,则是使用一个 `match` 表达式,或某个 `Result` 的那些方法,来以某种恰当方式对这个 `Result` 进行处理了。 这个错误消息还提到,`?` 还可与 `Option` 类型的值一同使用。与在 `Result` 上使用 `?` 一样,可在返回一个 `Option` 的函数中的 `Option` 上使用 `?`。在某个 `Option` 上调用 `?` 操作符的行为,与在 `Result` 上其被调用时的行为类似:在该值为 `None` 时,`None` 就会在那个地方及早地从该函数被返回。而在该值为 `Some` 时,那么这个 `Some` 内部的值,就是该表达式的结果值,同时函数会继续执行。下面清单 9-11 有着一个在给定文本中找到第一行最后一个字符的函数示例: ```rust fn last_char_of_first_line(text: &str) -> Option { text.lines().next()?.chars().last() } ``` *清单 9-11:在某个 `Option` 的值上使用 `?` 操作符* 由于可能那里有个字符,不过同样坑能那里没有字符,因此此函数返回的是 `Option`。这个代码取那个 `text` 字符串切片参数,并在其上调用了 `lines` 方法,该方法返回的是对该字符串中那些文本行的一个迭代器。由于此函数是要对首个文本行进行检查,因此他调用了那个迭代器上的 `next`,来从迭代器上获取头一个值。在 `text` 为空字符串时,那么这个到 `next` 的调用,就会返回 `None`,这也就是这里使用 `?` 来停止这个 `last_char_of_first_line` 函数,并自其返回 `None` 的情形。而在 `text` 不为空字符串时,`next` 就会返回一个包含了在 `text` 中第一行文本的字符串切片的 `Some` 值。 此时 `?` 操作符会提取这个字符串切片,进而就可以在那个字符串切片上调用 `chars`,来获取到他那些字符的一个迭代器。这里关心的是第一行文本中的最后一个字符,因此就要调用 `last` 来返回迭代器中的最后一个条目。因为首个文本行为空字符串是可能的,比如在 `text` 以空行开头却在其他行上有一些字符,如同在 `"\nhi"` 中一样,因此 `last` 得到一个就是个 `Option` 值。不过在首行上有最后一个字符时,这个字符就会在 `Some` 变种里被返回。中间的 `?` 操作符,给到了一种表达此逻辑的简洁方式,运行在一个行里来实现该函数。若无法在 `Option` 上运用这个 `?` 操作符,那么就必须使用更多方法调用,或 `match` 表达式来实现此逻辑。 注意在返回 `Result` 函数中的 `Result` 上,可以使用 `?` 操作符,而在返回 `Option` 函数中的 `Option` 上,可使用 `?` 操作符,但不能混用及进行匹配。`?` 操作符不会自动将 `Result` 转换为 `Option`,或反过来将 `Option` 转换为 `Result`;在这些情况下,是可以在 `Result` 上使用诸如 `ok`,或在 `Option` 上使用 `ok_or` 这样的方法,来显示地完成转换。 到目前为止,这里使用过的所有 `main` 函数,返回的都是 `()`。由于 `main` 函数是可执行程序的进入与退出点,因此他是特殊的,而关于其返回值类型可以是什么,为了程序如预期那样执行,是有一些限制的。 幸运的是,`main` 函数同样可以返回 `Result<(), E>`。下面清单 9-12 有着来自 9-10 的代码,不过这里将 `main` 函数的返回值类型,改成了 `Result<(), Box>`,并在最后添加了一个返回值 `Ok(())`。现在该代码就会编译了: ```rust use std::error::Error; use std::fs::File; fn main() -> Result<(), Box> { let greeting_file = File::open("hello.txt")?; Ok(()) } ``` *清单 9-12:将 `main` 修改为返回 `Result<(), E>`,就实现了在 `Result` 值上 `?` 操作符的使用* 这里的 `Box` 类型,是个 *特质对象(trait object)*,在第 17 章中的 [“运用特质对象实现不同类型的值”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#using-trait-objects-that-allow-for-values-of-different-types) 小节,就会讲到这个特性。而现在,可将 `Box` 理解为表示 “任何类别的错误”。由于 `?` 操作符允许将任何 `Err` 值及早返回,因此将 `?` 用在有着错误类型 `Box` 的 `main` 函数中, 某个 `Result` 值上是允许的。即使这个 `main` 函数的函数体,将只会返回类型 `std::io::Error` 的那些错误,而经由指定 `Box`,即使将返回其他错误的代码添加到 `main` 的函数体,该函数签名 `fn main() -> Result<(), Box>` 仍将无误。 在 `main` 函数返回了一个 `Result<(), E>` 时,那么若 `main` 返回的是 `Ok(())`,则该可执行程序就会以值 `0` 退出,并在 `main` 返回 `Err` 值时,以非零值退出。C 语言编写的可执行程序,在退出时返回的是些整数:成功退出的程序返回整数 `0`,而出错的程序返回某些非 `0` 的整数。Rust 从可执行程序返回的也是整数,从而与此约定兼容。 `main` 函数可能返回任何实现了 [`std::process::Termination` 特质(the `std::process::Termination`)](https://doc.rust-lang.org/std/process/trait.Termination.html) 的任何类型,该特质包含了返回某个 `ExitCode` 的 `report` 函数。请参考标准库文档,了解更多有关实现自己类型 `Termination` 的信息。 现在既然已经讨论了调用 `panic!` 或返回 `Result` 的细节,那么就要回到怎样判断,在何种情形下,使用哪种方式属于恰当的话题了。 ## 要 `panic!` 还是不要 `panic!` 那么该怎样确定,什么时候应该调用 `panic!`,以及什么时候应该返回 `Result` 呢?在代码中止时,就没有办法恢复了。当然可以在任何错误情形下调用 `panic!`,而不管存不存在可能的恢复方式,不过这个时候就是代码编写者本人,代替代码在做出这种情形为不可恢复的确定了。而在选择了返回某个 `Result` 值是,就赋予了调用代码(the calling code)各种选项。调用代码就可以根据其自身情况,而选择尝试恢复,或者他可以决定在此情形下的某个 `Err` 是不可恢复的,进而他就可以调用 `panic!` 而将可恢复错误,转变为不可恢复错误。这样看来,在对某个可能失败的函数进行定义时,返回一个 `Result` 就是良好的默认选择。 而在示例程序、原型代码及测试等中,那么比起返回 `Result`,编写程序中止代码就要更合适。接下来就要探讨一下为何这样讲,随后就要讨论一些编译器无法搞清楚,但作为代码编写者的人类却明白程序失败不可能发生的情形。本章将以一些有关在库代码中,如何确定要不要中止程序的守则结束(in situations such as examples, prototype code, and tests, it's more approciate to write code that panics instead of returning a `Result`. Let's explore why, then discuss situations in which the compiler can't tell that failure is impossible, but you as a human can. The chapter will conclude with some general guidelines on how to decide whether to panic in library code)。 ### 示例程序、原型代码与测试 在编写用于演示某些概念的示例程序时,若同时包含一些健壮的错误处理代码,就会令到示例程序不那么明晰。在示例程序中,到某个诸如 `unwrap` 这样的可能会中止程序运行方法的调用,确信就表明那是一个是要这个应用程序对错误进行处理的占位符,而根据接下来代码所做的事情,这样的调用会有所不同。 与此类似,`unwrap` 与 `expect` 方法在构造原型程序,尚未准备好确定如何处理错误时,是十分方便的。这些方法在代码中留下了清楚的一些记号,这些记号在已做好准备让程序更为健壮时会用到。 而当方法调用在测试中失败时,即使那个方法并非正在测试的某些功能,也会希望整个测试失败。由于 `panic!` 正是将测试标记为失败的方式,那么调用 `unwrap` 或 `expect`,则正是要用到的了。 ### 相比于编译器,代码编写者掌握了更多信息的情形 在有着确保了 `Result` 将有着 `Ok` 值的其他某些逻辑,但编译器对此逻辑却一无所知时,调用 `unwrap` 或 `expect` 也是恰当的。这时将仍然有个需要处理的 `Result` 值:对于不论所调用的什么操作,即使在当前特定情形下,逻辑上失败绝无可能,但所调用的操作原本总体上仍是有失败可能。在经由亲自检查代码,而能确保绝不会有 `Err` 变种时,调用 `unwrap` 就是完美可接受的,且将自己设想的绝不会有 `Err` 变种的原因,在 `expect` 文本中撰写出来,这样做甚至更佳。下面就是一个示例: ```rust fn main () { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("硬编码的 IP 地址应是有效的"); } ``` 这里是在通过解析硬编码字符串,创建一个 `IpAddr`。可以看到 `127.0.0.1` 是个有效的 IP 地址,因此这里使用 `expect` 是可接受的。然而,有着一个硬编码的、有效的字符串,并未改变 `parse` 方法返回值类型:这里仍将得到一个 `Result` 类型,同时由于编译器不是足够聪明到发现这个字符串总是个有效的 IP 地址,那么编译器仍将要求,以 `Err` 变种是一种可能性那样,对这个 `Result` 进行处理。在这个 IP 地址为来自用户输入,而非这里的硬编码到程序中,进而 *确实* 有着失败可能时,无疑就要打算对这个 `Result` 进行更为健壮的处理了。这种提及这个 IP 地址为硬编码的假设,将提醒到在将来,需要从其他来源获取这个 IP 地址时,就要把 `expect` 修改为更好的错误处理代码。 ### 错误处理守则 在代码可能以糟糕状态结束运行时,那么让代码中止运行就是明智的。在这种情形下,所谓 *糟糕状态(a bad state)* 就是在某种假设、保证、合约,或恒值已被破坏,譬如在无效值、矛盾值,或缺失值被传递到所编写代码 -- 加上以下的一项或多项: - 糟糕状态是某些不期望的东西,他们与偶发的东西相反,比如用户输入的错误格式数据; - 在此处之后的代码,需要依赖于不处在这种糟糕状态,而不是在接下来的每一步都检查这个问题; - 没有以自己所使用的类型,来编码该信息的好办法。在第 17 章的 [“将状态于行为编码为类型”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#encoding-states-and-behavior-as-types) 小节,就会贯穿一个这里所意指的示例。 在有人调用到咱们的代码,并传入了无意义的值时,在可以的情况下,最好返回一个错误,这样库用户就可以确定在那样的情况下,他们打算做什么。然而在继续执行下去会不安全或有危害的情形中,那么最佳选择就会时调用 `panic!`,并警醒使用到咱们库的人他们代码中的错误,这样在他们开发过程中就可以修好那个代码错误。与此类似,在调用不在掌控中的外部代码,且该外部代码返回了无法修复的无效状态时,那么 `panic!` 通常就是恰当选择。 不过在失败为预期的时,那么相比于构造一个 `panic!` 调用,返回一个 `Result` 则更为恰当。这类示例包括给到解析器错误格式数据,或某个返回了表示已达到访问数限制的 HTTP 请求等。在这些情况下,返回一个 `Result` 就表示失败是一种调用代码必须确定如何处理的预期可能。 在所编写代码被使用无效值调用,而执行了某种可能将用户置于危险境地的操作时,那么代码就应首先对这些值进行检查,并在这些值无效时中止运行。这主要是处于安全原因:尝试运行于无效数据,就会将代码暴露于漏洞。这就是在尝试超出边界的内存访问时,标准库会调用 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存,是个常见的安全问题。函数通常有着 *合约(contracts)*:只在输入满足特定要求时,他们的行为才有保证。那么由于合约破坏总是表明调用者侧的代码错误,且这种错误并非是要调用代码必须显式处理的那种错误,因此在合约被破坏时的中止运行就说得通了。实际上,调用代码是没有恢复的合理方法的;调用的 *代码编写者* 需要修复该代码。应在函数的 API 文档中,解释函数的合约,尤其是在合约破坏会导致中止运行时。 但是,在全部的函数中,进行大量错误检查,则会显得冗长而烦人。幸运的是,可使用 Rust 的类型系统(并因此由编译器完成类型检查),来完成许多的检查。在函数有着作为参数的特定类型时,就可以在知悉编译器已经确保有着有效值的情况下,着手处理代码的业务逻辑。比如,在有着一个不同于 `Option` 的类型时,程序就期望有 *某个东西(something)* 而非 *什么也没有(nothing)*。代码这时就不必处理 `Some` 与 `None` 变种的两种情形:无疑将只有一种有着某个值的情形。尝试将无值传递给该函数的代码,甚至都不会编译,那么该函数就不必在运行时对那样的情况进行检查了。另一个示例则是使用某个诸如 `u32` 无符号整数,这就确保了参数绝不会是个负数。 ### 创建用于验证的定制类型 接下来将这个运用 Rust 的类型系统,来确保有着有效值的概念,进行进一步拓展,而看看创建一个用于验证的定制类型。回顾在第二章中的猜数游戏,其中的代码要求用户猜出一个 `1` 与 `100` 之间的数字。在将用户猜的数字与那里的秘密数字比对之前,是绝无对用户猜数是否处于 `1` 与 `100` 之间,进行过验证的;那里只验证过猜数为正数。在这个示例中,后果并不是非常可怕:这里的输出 “太大了” 或 “太小了” 仍将正确。但引导用户朝向有效的猜数,并在用户猜出不在该范围的数,与用户敲入了比如一些字母时,而有不同的表现,将是一项有用的功能增强。 完成此功能增强的一种方式,将是将猜数解析为一个 `i32` 而非仅仅为一个 `u32`,从而允许潜在的负数,并在随后键入一个该数位于范围中的检查,像下面这样: ```rust loop { // --跳过-- let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println! ("请输入一个数字!"); continue }, }; if guess < 1 || guess > 100 { println! ("秘密数字将在 1 和 100 之间"); continue; } match guess.cmp(&secret_number) { // --跳过-- } ``` 其中的 `if` 表达式,对这里的值是否超出范围进行了检查,告诉用户这个问题,并调用 `continue` 来开始下一次循环迭代而请求另一个猜数。在这个 `if` 表达式之后,就可继续进行 `guess` 与秘密数字之间的比较,获悉 `guess` 是在 `1` 与 `100` 之间。 然而这并非一种理想的方案:若程序只运行在 `1` 与 `100` 之间的值这一点至关重要,且程序有着许多有此要求的函数,而在每个函数中都进行这样的一个检查,就会显得冗长乏味(并可能影响性能)。 相反,这里可以构造一种新类型,并将那些验证放入某个函数,从而创建出该类型的一个示例,而非在各个地方重复这些验证。那样的话,这些函数就可以在他们的签名中,安全地使用这种新类型,并信心十足地使用他们接收到的那些值了。下面清单 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 类型值必须在 1 与 100 之间,收到的是 {}", value); } Guess { value } } pub fn value(&self) -> i32 { self.value } } ``` *清单 9-13:只会在值处于 `1` 与 `100` 之间,才继续执行的一个 `Guess` 类型* 首先,这里定义了一个名为 `Guess`,带有一个叫做 `value`、保存了一个 `i32` 值字段的结构体。这就是要存储数字的地方。 随后这里在 `Guess` 上实现了一个名为 `new` 的关联函数,其创建出一个 `Guess` 类型的实例。这个 `new` 函数被定义为有着一个名为 `value`、类型为 `i32` 的参数,以及要返回一个 `Guess` 类型值。`new` 函数体中的代码,对 `value` 进行了测试,从而确保 `value` 是在 `1` 与 `100` 之间。若 `value` 未通过此测试,那么就做出一个 `panic!` 调用,由于创建一个超出此范围的 `Guess` 会破坏 `Guess::new` 所依赖的合约,因此这就会警醒到编写调用代码的程序员,他们有个需要修复的代码错误。`Guess::new` 可能中止运行的条件,应在其公开的 API 文档中,进行说明。在第 14 章就会涉及到在所创建的文档中,表示 `panic!` 可能性的一些约定。在 `value` 通过该测试时,这里就会创建一个将 `value` 字段设置为那个 `value` 参数的新 `Guess` 类型值,并返回这个 `Guess` 类型值。 接下来,这里实现了一个名为 `value`、借用了 `self`,不带任何其他参数,并返回一个 `i32` 的方法。由于这类方法的目的,是要从一些字段获取数据并加以返回,因此有时就被叫做 *取值方法(getter)*。因为 `Guess` 结构体的这个 `value` 字段是私有的,那么这个公开方法就是必要的。这个 `value` 字段作为私有至关重要,这样使用这个 `Guess` 结构体的代码就不被允许直接设置 `value`:该模组外部的代码,*必须* 使用 `Guess::new` 函数,来创建 `Guess` 的实例,这样就确保了 `Guess` 不会有未经 `Guess::new` 函数中条件检查的 `value`。 现在某个有着一个 `1` 与 `100` 之间参数,或只返回 `1` 与 `100` 之间数字的函数,就可以在其函数签名中,声明他所取参数或其返回值为 `Guess` 类型而非 `i32` 类型,而不再需要在其函数体中完成任何额外检查了。 ## 小结 Rust 的那些错误处理特性,被设计用于帮助编写更为健壮的代码。`panic!` 这个宏,发出了程序处于其无法处理状态的信号,并让咱们告知进程停下来,而不是尝试以无效或不正确的一些值继续运行。而 `Result` 这个枚举则使用了 Rust 的类型系统,来表示以代码可以从中恢复过来的某种方式的一些操作失败(the `Result` enum uses Rust's type system to indicate that operations might fail in a way that your code could recover from)。还可使用 `Result` 来告诉调用了咱们代码的代码,需要处理潜在的成功与失败情形。在一些适当情形下,运用 `panic!` 与 `Result` 就会令到咱们的代码在各种不可避免的问题面前,更加可靠。 既然这里已经见识到标准库在 `Option` 与 `Result` 枚举上,运用到泛型的一些有用方式,那么接下来就要谈及泛型的原理,以及怎样在咱们的代码中运用泛型。