@ -47,7 +47,7 @@
< / script >
< / script >
< div id = "sidebar" class = "sidebar" >
< div id = "sidebar" class = "sidebar" >
< ul class = "chapter" > < li > < a href = "ch01-00-introduction.html" > < strong > 1.< / strong > 介绍< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch01-01-installation.html" > < strong > 1.1.< / strong > 安装< / a > < / li > < li > < a href = "ch01-02-hello-world.html" > < strong > 1.2.< / strong > Hello, World!< / a > < / li > < / ul > < / li > < li > < a href = "ch02-00-guessing-game-tutorial.html" > < strong > 2.< / strong > 猜猜看教程< / a > < / li > < li > < a href = "ch03-00-common-programming-concepts.html" > < strong > 3.< / strong > 通用编程概念< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch03-01-variables-and-mutability.html" > < strong > 3.1.< / strong > 变量和可变性< / a > < / li > < li > < a href = "ch03-02-data-types.html" > < strong > 3.2.< / strong > 数据类型< / a > < / li > < li > < a href = "ch03-03-how-functions-work.html" > < strong > 3.3.< / strong > 函数如何工作< / a > < / li > < li > < a href = "ch03-04-comments.html" > < strong > 3.4.< / strong > 注释< / a > < / li > < li > < a href = "ch03-05-control-flow.html" > < strong > 3.5.< / strong > 控制流< / a > < / li > < / ul > < / li > < li > < a href = "ch04-00-understanding-ownership.html" > < strong > 4.< / strong > 认识所有权< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch04-01-what-is-ownership.html" > < strong > 4.1.< / strong > 什么是所有权< / a > < / li > < li > < a href = "ch04-02-references-and-borrowing.html" > < strong > 4.2.< / strong > 引用 & 借用< / a > < / li > < li > < a href = "ch04-03-slices.html" > < strong > 4.3.< / strong > Slices< / a > < / li > < / ul > < / li > < li > < a href = "ch05-00-structs.html" > < strong > 5.< / strong > 结构体< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch05-01-method-syntax.html" > < strong > 5.1.< / strong > 方法语法< / a > < / li > < / ul > < / li > < li > < a href = "ch06-00-enums.html" > < strong > 6.< / strong > 枚举和模式匹配< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch06-01-defining-an-enum.html" > < strong > 6.1.< / strong > 定义枚举< / a > < / li > < li > < a href = "ch06-02-match.html" > < strong > 6.2.< / strong > < code > match< / code > 控制流运算符< / a > < / li > < li > < a href = "ch06-03-if-let.html" > < strong > 6.3.< / strong > < code > if let< / code > 简单控制流< / a > < / li > < / ul > < / li > < li > < a href = "ch07-00-modules.html" > < strong > 7.< / strong > 模块< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch07-01-mod-and-the-filesystem.html" > < strong > 7.1.< / strong > < code > mod< / code > 和文件系统< / a > < / li > < li > < a href = "ch07-02-controlling-visibility-with-pub.html" > < strong > 7.2.< / strong > 使用< code > pub< / code > 控制可见性< / a > < / li > < li > < a href = "ch07-03-importing-names-with-use.html" > < strong > 7.3.< / strong > 使用< code > use< / code > 导入命名< / a > < / li > < / ul > < / li > < li > < a href = "ch08-00-common-collections.html" > < strong > 8.< / strong > 通用集合类型< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch08-01-vectors.html" > < strong > 8.1.< / strong > vector< / a > < / li > < li > < a href = "ch08-02-strings.html" > < strong > 8.2.< / strong > 字符串< / a > < / li > < li > < a href = "ch08-03-hash-maps.html" > < strong > 8.3.< / strong > 哈希 map< / a > < / li > < / ul > < / li > < / ul >
< ul class = "chapter" > < li > < a href = "ch01-00-introduction.html" > < strong > 1.< / strong > 介绍< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch01-01-installation.html" > < strong > 1.1.< / strong > 安装< / a > < / li > < li > < a href = "ch01-02-hello-world.html" > < strong > 1.2.< / strong > Hello, World!< / a > < / li > < / ul > < / li > < li > < a href = "ch02-00-guessing-game-tutorial.html" > < strong > 2.< / strong > 猜猜看教程< / a > < / li > < li > < a href = "ch03-00-common-programming-concepts.html" > < strong > 3.< / strong > 通用编程概念< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch03-01-variables-and-mutability.html" > < strong > 3.1.< / strong > 变量和可变性< / a > < / li > < li > < a href = "ch03-02-data-types.html" > < strong > 3.2.< / strong > 数据类型< / a > < / li > < li > < a href = "ch03-03-how-functions-work.html" > < strong > 3.3.< / strong > 函数如何工作< / a > < / li > < li > < a href = "ch03-04-comments.html" > < strong > 3.4.< / strong > 注释< / a > < / li > < li > < a href = "ch03-05-control-flow.html" > < strong > 3.5.< / strong > 控制流< / a > < / li > < / ul > < / li > < li > < a href = "ch04-00-understanding-ownership.html" > < strong > 4.< / strong > 认识所有权< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch04-01-what-is-ownership.html" > < strong > 4.1.< / strong > 什么是所有权< / a > < / li > < li > < a href = "ch04-02-references-and-borrowing.html" > < strong > 4.2.< / strong > 引用 & 借用< / a > < / li > < li > < a href = "ch04-03-slices.html" > < strong > 4.3.< / strong > Slices< / a > < / li > < / ul > < / li > < li > < a href = "ch05-00-structs.html" > < strong > 5.< / strong > 结构体< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch05-01-method-syntax.html" > < strong > 5.1.< / strong > 方法语法< / a > < / li > < / ul > < / li > < li > < a href = "ch06-00-enums.html" > < strong > 6.< / strong > 枚举和模式匹配< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch06-01-defining-an-enum.html" > < strong > 6.1.< / strong > 定义枚举< / a > < / li > < li > < a href = "ch06-02-match.html" > < strong > 6.2.< / strong > < code > match< / code > 控制流运算符< / a > < / li > < li > < a href = "ch06-03-if-let.html" > < strong > 6.3.< / strong > < code > if let< / code > 简单控制流< / a > < / li > < / ul > < / li > < li > < a href = "ch07-00-modules.html" > < strong > 7.< / strong > 模块< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch07-01-mod-and-the-filesystem.html" > < strong > 7.1.< / strong > < code > mod< / code > 和文件系统< / a > < / li > < li > < a href = "ch07-02-controlling-visibility-with-pub.html" > < strong > 7.2.< / strong > 使用< code > pub< / code > 控制可见性< / a > < / li > < li > < a href = "ch07-03-importing-names-with-use.html" > < strong > 7.3.< / strong > 使用< code > use< / code > 导入命名< / a > < / li > < / ul > < / li > < li > < a href = "ch08-00-common-collections.html" > < strong > 8.< / strong > 通用集合类型< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch08-01-vectors.html" > < strong > 8.1.< / strong > vector< / a > < / li > < li > < a href = "ch08-02-strings.html" > < strong > 8.2.< / strong > 字符串< / a > < / li > < li > < a href = "ch08-03-hash-maps.html" > < strong > 8.3.< / strong > 哈希 map< / a > < / li > < / ul > < / li > < li > < a href = "ch09-00-error-handling.html" > < strong > 9.< / strong > 错误处理< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch09-01-unrecoverable-errors-with-panic.html" > < strong > 9.1.< / strong > < code > panic!< / code > 与不可恢复的错误< / a > < / li > < li > < a href = "ch09-02-recoverable-errors-with-result.html" > < strong > 9.2.< / strong > < code > Result< / code > 与可恢复的错误< / a > < / li > < li > < a href = "ch09-03-to-panic-or-not-to-panic.html" > < strong > 9.3.< / strong > < code > panic!< / code > 还是不< code > panic!< / code > < / a > < / li > < / ul > < / li > < li > < a href = "ch10-00-generics.html" > < strong > 10.< / strong > 泛型、trait 和生命周期< / a > < / li > < li > < ul class = "section" > < li > < a href = "ch10-01-syntax.html" > < strong > 10.1.< / strong > 泛型数据类型< / a > < / li > < li > < a href = "ch10-02-traits.html" > < strong > 10.2.< / strong > trait: 定义共享的行为< / a > < / li > < li > < a href = "ch10-03-lifetime-syntax.html" > < strong > 10.3.< / strong > 生命周期与引用有效性< / a > < / li > < / ul > < / li > < / ul >
< / div >
< / div >
< div id = "page-wrapper" class = "page-wrapper" >
< div id = "page-wrapper" class = "page-wrapper" >
@ -4109,6 +4109,595 @@ println!("{:?}", map);
< / ul >
< / ul >
< p > 标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!< / p >
< p > 标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!< / p >
< p > 我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!< / p >
< p > 我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!< / p >
< h1 > 错误处理< / h1 >
< blockquote >
< p > < a href = "https://github.com/rust-lang/book/blob/master/src/ch09-00-error-handling.md" > ch09-00-error-handling.md< / a >
< br >
commit fc825966fabaa408067eb2df3aa45e4fa6644fb6< / p >
< / blockquote >
< p > Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下, Rust 要求你承认出错的可能性可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。< / p >
< p > Rust 将错误组合成两个主要类别:< strong > 可恢复错误< / strong > ( < em > recoverable< / em > )和< strong > 不可恢复错误< / strong > ( < em > unrecoverable< / em > )。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。< / p >
< p > 大部分语言并不区分这两类错误, 并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有< code > Result< T, E> < / code > 值和< code > panic!< / code > ,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍< code > panic!< / code > 调用,接着会讲到如何返回< code > Result< T, E> < / code > 。最后,我们会讨论当决定是尝试从错误中恢复还是停止执行时需要顾及的权衡考虑。< / p >
< h2 > < code > panic!< / code > 与不可恢复的错误< / h2 >
< blockquote >
< p > < a href = "https://github.com/rust-lang/book/blob/master/src/ch09-01-unrecoverable-errors-with-panic.md" > ch09-01-unrecoverable-errors-with-panic.md< / a >
< br >
commit 380e6ee57c251f5ffa8df4c58b3949405448d914< / p >
< / blockquote >
< p > 突然有一天, 糟糕的事情发生了, 而你对此束手无策。对于这种情况, Rust 有`panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,并接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。< / p >
< blockquote >
< h3 > Panic 中的栈展开与终止< / h3 >
< p > 当出现< code > panic!< / code > 时,程序默认会开始< strong > 展开< / strong > ( < em > unwinding< / em > ),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接< strong > 终止< / strong > ( < em > abort< / em > ),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 < em > Cargo.toml< / em > 的< code > [profile]< / code > 部分增加< code > panic = 'abort'< / code > 。例如,如果你想要在发布模式中 panic 时直接终止:< / p >
< pre > < code class = "language-toml" > [profile.release]
panic = 'abort'
< / code > < / pre >
< / blockquote >
< p > 让我们在一个简单的程序中调用< code > panic!< / code > : < / p >
< p > < span class = "filename" > Filename: src/main.rs< / span > < / p >
< pre > < code class = "language-rust,should_panic" > fn main() {
panic!(" crash and burn" );
}
< / code > < / pre >
< p > 运行程序将会出现类似这样的输出:< / p >
< pre > < code > $ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished debug [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)
< / code > < / pre >
< p > 最后三行包含< code > panic!< / code > 造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:< em > src/main.rs:2< / em > 表明这是 < em > src/main.rs< / em > 文件的第二行。< / p >
< p > 在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现< code > panic!< / code > 宏的调用。换句话说,< code > panic!< / code > 可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的< code > panic!< / code > 宏调用,而不是我们代码中最终导致< code > panic!< / code > 的那一行。可以使用< code > panic!< / code > 被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。< / p >
< h3 > 使用< code > panic!< / code > backtrace< / h3 >
< p > 让我们来看看另一个因为我们代码中的 bug 引起的别的库中< code > panic!< / code > 的例子,而不是直接的宏调用:< / p >
< p > < span class = "filename" > Filename: src/main.rs< / span > < / p >
< pre > < code class = "language-rust,should_panic" > fn main() {
let v = vec![1, 2, 3];
v[100];
}
< / code > < / pre >
< p > 这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。< code > []< / code > 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。< / p >
< p > 这种情况下其他像 C 这样语言会尝直接试提供所要求的值,即便这可能不是你期望的:你会得到对任何应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为< strong > 缓冲区溢出< / strong > ( < em > buffer overread< / em > ),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。< / p >
< p > 为了使程序远离这类漏洞, 如果尝试读取一个索引不存在的元素, Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:< / p >
< pre > < code > $ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished debug [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
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
< / code > < / pre >
< p > 这指向了一个不是我们编写的文件,< em > libcollections/vec.rs< / em > 。这是标准库中< code > Vec< T> < / code > 的实现。这是当对 vector < code > v< / code > 使用< code > []< / code > 时 < em > libcollections/vec.rs< / em > 中会执行的代码,也是真正出现< code > panic!< / code > 的地方。< / p >
< p > 接下来的几行提醒我们可以设置< code > RUST_BACKTRACE< / code > 环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:< / p >
< figure >
< pre > < code > $ RUST_BACKTRACE=1 cargo run
Finished debug [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:1395
stack backtrace:
1: 0x10922522c -
std::sys::imp::backtrace::tracing::imp::write::h1204ab053b688140
2: 0x10922649e -
std::panicking::default_hook::{{closure}}::h1204ab053b688140
3: 0x109226140 - std::panicking::default_hook::h1204ab053b688140
4: 0x109226897 -
std::panicking::rust_panic_with_hook::h1204ab053b688140
5: 0x1092266f4 - std::panicking::begin_panic::h1204ab053b688140
6: 0x109226662 - std::panicking::begin_panic_fmt::h1204ab053b688140
7: 0x1092265c7 - rust_begin_unwind
8: 0x1092486f0 - core::panicking::panic_fmt::h1204ab053b688140
9: 0x109248668 -
core::panicking::panic_bounds_check::h1204ab053b688140
10: 0x1092205b5 - < collections::vec::Vec< T> as
core::ops::Index< usize> > ::index::h1204ab053b688140
11: 0x10922066a - panic::main::h1204ab053b688140
12: 0x1092282ba - __rust_maybe_catch_panic
13: 0x109226b16 - std::rt::lang_start::h1204ab053b688140
14: 0x1092206e9 - main
< / code > < / pre >
< figcaption >
< p > Listing 9-1: The backtrace generated by a call to < code > panic!< / code > displayed when the
environment variable < code > RUST_BACKTRACE< / code > is set< / p >
< / figcaption >
< / figure >
< p > 这里有大量的输出! backtrace 第 11 行指向了我们程序中引起错误的行:< em > src/main.rs< / em > 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。< / p >
< p > 如果你不希望我们的程序 panic, 第一个提到我们编写的代码行的位置是你应该开始调查的, 以便查明是什么值如何在这个地方引起了 panic。在上面的例子中, 我们故意编写会 panic 的代码来演示如何使用 backtrace, 修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你得代码出现了 panic, 你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic, 以及应当如何处理才能避免这个问题。< / p >
< p > 本章的后面会再次回到< code > panic!< / code > 并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用< code > Result< / code > 来从错误中恢复。< / p >
< h2 > < code > Result< / code > 与可恢复的错误< / h2 >
< blockquote >
< p > < a href = "https://github.com/rust-lang/book/blob/master/src/ch09-02-recoverable-errors-with-result.md" > ch09-01-unrecoverable-errors-with-panic.md< / a >
< br >
commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1< / p >
< / blockquote >
< p > 大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并回应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。< / p >
< p > 回忆一下第二章“使用< code > Result< / code > 类型来处理潜在的错误”部分中的那个< code > Result< / code > 枚举,它定义有如下连个成员,< code > Ok< / code > 和< code > Err< / code > : < / p >
< pre > < code class = "language-rust" > enum Result< T, E> {
Ok(T),
Err(E),
}
< / code > < / pre >
< p > < code > T< / code > 和< code > E< / code > 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是< code > T< / code > 代表成功时返回的< code > Ok< / code > 成员中的数据的类型,而< code > E< / code > 代表失败时返回的< code > Err< / code > 成员中的错误的类型。因为< code > Result< / code > 有这些泛型类型参数,我们可以将< code > Result< / code > 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。< / p >
< p > 让我们调用一个返回< code > Result< / code > 的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-rust" > use std::fs::File;
fn main() {
let f = File::open(" hello.txt" );
}
< / code > < / pre >
< figcaption >
< p > Listing 9-2: Opening a file< / p >
< / figcaption >
< / figure >
< p > 如何知道< code > File::open< / code > 返回一个< code > Result< / code > 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给< code > f< / code > 某个我们知道< strong > 不是< / strong > 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们< code > f< / code > 的类型< strong > 应该< / strong > 是什么,为此我们将< code > let f< / code > 语句改为:< / p >
< pre > < code class = "language-rust,ignore" > let f: u32 = File::open(" hello.txt" );
< / code > < / pre >
< p > 现在尝试编译会给出如下错误:< / p >
< pre > < code > 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`
= note: found type `std::result::Result< std::fs::File, std::io::Error> `
< / code > < / pre >
< p > 这就告诉我们了< code > File::open< / code > 函数的返回值类型是< code > Result< T, E> < / code > 。这里泛型参数< code > T< / code > 放入了成功值的类型< code > std::fs::File< / code > ,它是一个文件句柄。< code > E< / code > 被用在失败值上其类型是< code > std::io::Error< / code > 。< / p >
< p > 这个返回值类型说明< code > File::open< / code > 调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。< code > File::open< / code > 需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是< code > Result< / code > 枚举可以提供的。< / p >
< p > 当< code > File::open< / code > 成功的情况下,变量< code > f< / code > 的值将会是一个包含文件句柄的< code > Ok< / code > 实例。在失败的情况下,< code > f< / code > 会是一个包含更多关于出现了何种错误信息的< code > Err< / code > 实例。< / p >
< p > 我们需要在列表 9-2 的代码中增加根据< code > File::open< / code > 返回值进行不同处理的逻辑。列表 9-3 展示了一个处理< code > Result< / code > 的基本工具:第六章学习过的< code > match< / code > 表达式。< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-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!(" There was a problem opening the file: {:?}" , error)
},
};
}
< / code > < / pre >
< figcaption >
< p > Listing 9-3: Using a < code > match< / code > expression to handle the < code > Result< / code > variants we
might have< / p >
< / figcaption >
< / figure >
< p > 注意与< code > Option< / code > 枚举一样,< code > Result< / code > 枚举和其成员也被导入到了 prelude 中,所以就不需要在< code > match< / code > 分支中的< code > Ok< / code > 和< code > Err< / code > 之前指定< code > Result::< / code > 。< / p >
< p > 这里我们告诉 Rust 当结果是< code > Ok< / code > ,返回< code > Ok< / code > 成员中的< code > file< / code > 值,然后将这个文件句柄赋值给变量< code > f< / code > 。< code > match< / code > 之后,我们可以利用这个文件句柄来进行读写。< / p >
< p > < code > match< / code > 的另一个分支处理从< code > File::open< / code > 得到< code > Err< / code > 值的情况。在这种情况下,我们选择调用< code > panic!< / code > 宏。如果当前目录没有一个叫做 < em > hello.txt< / em > 的文件,当运行这段代码时会看到如下来自< code > panic!< / code > 宏的输出:< / p >
< pre > < code > 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
< / code > < / pre >
< h3 > 匹配不同的错误< / h3 >
< p > 列表 9-3 中的代码不管< code > File::open< / code > 是因为什么原因失败都会< code > panic!< / code > 。我们真正希望的是对不同的错误原因采取不同的行为:如果< code > File::open< / code > 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果< code > File::open< / code > 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样< code > panic!< / code > 。让我们看看列表 9-4, 其中< code > match< / code > 增加了另一个分支:< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-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(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create(" hello.txt" ) {
Ok(fc) => fc,
Err(e) => {
panic!(
" Tried to create file but there was a problem: {:?}" ,
e
)
},
}
},
Err(error) => {
panic!(
" There was a problem opening the file: {:?}" ,
error
)
},
};
}
< / code > < / pre >
< figcaption >
< p > Listing 9-4: Handling different kinds of errors in different ways< / p >
< / figcaption >
< / figure >
< p > < code > File::open< / code > 返回的< code > Err< / code > 成员中的值类型< code > io::Error< / code > ,它是一个标准库中提供的结构体。这个结构体有一个返回< code > io::ErrorKind< / code > 值的< code > kind< / code > 方法可供调用。< code > io::ErrorKind< / code > 是一个标准库提供的枚举,它的成员对应< code > io< / code > 操作可能导致的不同错误类型。我们感兴趣的成员是< code > ErrorKind::NotFound< / code > ,它代表尝试打开的文件并不存在。< / p >
< p > < code > if error.kind() == ErrorKind::NotFound< / code > 条件被称作 < em > match guard< / em > :它是一个进一步完善< code > match< / code > 分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑< code > match< / code > 中的下一个分支。模式中的< code > ref< / code > 是必须的,这样< code > error< / code > 就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用< code > ref< / code > 而不是< code > & < / code > 来获取一个引用。简而言之,在模式的上下文中,< code > & < / code > 匹配一个引用并返回它的值,而< code > ref< / code > 匹配一个值并返回一个引用。< / p >
< p > 在 match guard 中我们想要检查的条件是< code > error.kind()< / code > 是否是< code > ErrorKind< / code > 枚举的< code > NotFound< / code > 成员。如果是,尝试用< code > File::create< / code > 创建文件。然而< code > File::create< / code > 也可能会失败,我们还需要增加一个内部< code > match< / code > 语句。当文件不能被打开,会打印出一个不同的错误信息。外部< code > match< / code > 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。< / p >
< h3 > 失败时 panic 的捷径:< code > unwrap< / code > 和< code > expect< / code > < / h3 >
< p > < code > match< / code > 能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。< code > Result< T, E> < / code > 类型定义了很多辅助方法来处理各种情况。其中之一叫做< code > unwrap< / code > ,它的实现就类似于列表 9-3 中的< code > match< / code > 语句。如果< code > Result< / code > 值是成员< code > Ok< / code > , < code > unwrap< / code > 会返回< code > Ok< / code > 中的值。如果< code > Result< / code > 是成员< code > Err< / code > , < code > unwrap< / code > 会为我们调用< code > panic!< / code > 。< / p >
< pre > < code class = "language-rust,should_panic" > use std::fs::File;
fn main() {
let f = File::open(" hello.txt" ).unwrap();
}
< / code > < / pre >
< p > 如果调用这段代码时不存在 < em > hello.txt< / em > 文件,我们将会看到一个< code > unwrap< / code > 调用< code > panic!< / code > 时提供的错误信息:< / p >
< pre > < code > 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
< / code > < / pre >
< p > 还有另一个类似于< code > unwrap< / code > 的方法它还允许我们选择< code > panic!< / code > 的错误信息:< code > expect< / code > 。使用< code > expect< / code > 而不是< code > unwrap< / code > 并提供一个好的错误信息可以表明你的意图并有助于追踪 panic 的根源。< code > expect< / code > 的语法看起来像这样:< / p >
< pre > < code class = "language-rust,should_panic" > use std::fs::File;
fn main() {
let f = File::open(" hello.txt" ).expect(" Failed to open hello.txt" );
}
< / code > < / pre >
< p > < code > expect< / code > 与< code > unwrap< / code > 的使用方式一样:返回文件句柄或调用< code > panic!< / code > 宏。< code > expect< / code > 用来调用< code > panic!< / code > 的错误信息将会作为传递给< code > expect< / code > 的参数,而不像< code > unwrap< / code > 那样使用默认的< code > panic!< / code > 信息。它看起来像这样:< / p >
< pre > < code > 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
< / code > < / pre >
< h3 > 传播错误< / h3 >
< p > 当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为< strong > 传播< / strong > ( < em > propagating< / em > )错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。< / p >
< p > 例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:< / p >
< figure >
< pre > < code class = "language-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),
}
}
< / code > < / pre >
< figcaption >
< p > Listing 9-5: A function that returns errors to the calling code using < code > match< / code > < / p >
< / figcaption >
< / figure >
< p > 首先让我们看看函数的返回值:< code > Result< String, io::Error> < / code > 。这意味着函数返回一个< code > Result< T, E> < / code > 类型的值,其中泛型参数< code > T< / code > 的具体类型是< code > String< / code > ,而< code > E< / code > 的具体类型是< code > io::Error< / code > 。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含< code > String< / code > 的< code > Ok< / code > 值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个< code > Err< / code > 值,它储存了一个包含更多这个问题相关信息的< code > io::Error< / code > 实例。我们选择< code > io::Error< / code > 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:< code > File::open< / code > 函数和< code > read_to_string< / code > 方法。< / p >
< p > 函数体以< code > File::open< / code > 函数开头。接着使用< code > match< / code > 处理返回值< code > Result< / code > ,类似于列表 9-3 中的< code > match< / code > ,唯一的区别是不再当< code > Err< / code > 时调用< code > panic!< / code > ,而是提早返回并将< code > File::open< / code > 返回的错误值作为函数的错误返回值传递给调用者。如果< code > File::open< / code > 成功了,我们将文件句柄储存在变量< code > f< / code > 中并继续。< / p >
< p > 接着我们在变量< code > s< / code > 中创建了一个新< code > String< / code > 并调用文件句柄< code > f< / code > 的< code > read_to_string< / code > 方法来将文件的内容读取到< code > s< / code > 中。< code > read_to_string< / code > 方法也返回一个< code > Result< / code > 因为它也可能会失败:哪怕是< code > File::open< / code > 已经成功了。所以我们需要另一个< code > match< / code > 来处理这个< code > Result< / code > :如果< code > read_to_string< / code > 成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进< code > Ok< / code > 的< code > s< / code > 中。如果< code > read_to_string< / code > 失败了,则像之前处理< code > File::open< / code > 的返回值的< code > match< / code > 那样返回错误值。并不需要显式的调用< code > return< / code > ,因为这是函数的最后一个表达式。< / p >
< p > 调用这个函数的代码最终会得到一个包含用户名的< code > Ok< / code > 值,亦或一个包含< code > io::Error< / code > 的< code > Err< / code > 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个< code > Err< / code > 值,他们可能会选择< code > panic!< / code > 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。< / p >
< p > 这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:< code > ?< / code > 。< / p >
< h3 > 传播错误的捷径:< code > ?< / code > < / h3 >
< p > 列表 9-6 展示了一个< code > read_username_from_file< / code > 的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符:< / p >
< figure >
< pre > < code class = "language-rust" > use std::io;
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)
}
< / code > < / pre >
< figcaption >
< p > Listing 9-6: A function that returns errors to the calling code using < code > ?< / code > < / p >
< / figcaption >
< / figure >
< p > < code > Result< / code > 值之后的< code > ?< / code > 被定义为与列表 9-5 中定义的处理< code > Result< / code > 值的< code > match< / code > 表达式有着完全相同的工作方式。如果< code > Result< / code > 的值是< code > Ok< / code > ,这个表达式将会返回< code > Ok< / code > 中的值而程序将继续执行。如果值是< code > Err< / code > , < code > Err< / code > 中的值将作为整个函数的返回值,就好像使用了< code > return< / code > 关键字一样,这样错误值就被传播给了调用者。< / p >
< p > 在列表 9-6 的上下文中,< code > File::open< / code > 调用结尾的< code > ?< / code > 将会把< code > Ok< / code > 中的值返回给变量< code > f< / code > 。如果出现了错误,< code > ?< / code > 会提早返回整个函数并将任何< code > Err< / code > 值传播给调用者。同理也适用于< code > read_to_string< / code > 调用结尾的< code > ?< / code > 。< / p >
< p > < code > ?< / code > 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在< code > ?< / code > 之后直接使用链式方法调用来进一步缩短代码:< / p >
< pre > < code class = "language-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)
}
< / code > < / pre >
< p > 在< code > s< / code > 中创建新的< code > String< / code > 被放到了函数开头;这没有什么变化。我们对< code > File::open(" hello.txt" )?< / code > 的结果直接链式调用了< code > read_to_string< / code > ,而不再创建变量< code > f< / code > 。仍然需要< code > read_to_string< / code > 调用结尾的< code > ?< / code > ,而且当< code > File::open< / code > 和< code > read_to_string< / code > 都成功没有失败时返回包含用户名< code > s< / code > 的< code > Ok< / code > 值。其功能再一次与列表 9-5 和列表 9-5 保持一致,不过这是一个与众不同且更符合工程学的写法。< / p >
< h3 > < code > ?< / code > 只能被用于返回< code > Result< / code > 的函数< / h3 >
< p > < code > ?< / code > 只能被用于返回值类型为< code > Result< / code > 的函数,因为他被定义为与列表 9-5 中的< code > match< / code > 表达式有着完全相同的工作方式。< code > match< / code > 的< code > return Err(e)< / code > 部分要求返回值类型是< code > Result< / code > ,所以函数的返回值必须是< code > Result< / code > 才能与这个< code > return< / code > 相兼容。< / p >
< p > 让我们看看在< code > main< / code > 函数中使用< code > ?< / code > 会发生什么,如果你还记得的话它的返回值类型是< code > ()< / code > : < / p >
< pre > < code class = "language-rust,ignore" > use std::fs::File;
fn main() {
let f = File::open(" hello.txt" )?;
}
< / code > < / pre >
<!-- NOTE: as of 2016 - 12 - 21, the error message when calling `?` in a function
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.
< p > 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
error message. /Carol --> < / p >
< p > 当编译这些代码,会得到如下错误信息:< / p >
< pre > < code > error[E0308]: mismatched types
-->
|
3 | let f = File::open(" hello.txt" )?;
| ^^^^^^^^^^^^^^^^^^^^^^^^^ expected (), found enum
`std::result::Result`
|
= note: expected type `()`
= note: found type `std::result::Result< _, _> `
< / code > < / pre >
< p > 错误指出存在不匹配的类型:< code > main< / code > 函数返回一个< code > ()< / code > 类型,而< code > ?< / code > 返回一个< code > Result< / code > 。编写不返回< code > Result< / code > 的函数时,如果调用其他返回< code > Result< / code > 的函数,需要使用< code > match< / code > 或者< code > Result< / code > 的方法之一来处理它,而不能用< code > ?< / code > 将潜在的错误传播给调用者。< / p >
< p > 现在我们讨论过了调用< code > panic!< / code > 或返回< code > Result< / code > 的细节,是时候返回他们各自适合哪些场景的话题了。< / p >
< h2 > < code > panic!< / code > 还是不< code > panic!< / code > < / h2 >
< blockquote >
< p > < a href = "https://github.com/rust-lang/book/blob/master/src/ch09-03-to-panic-or-not-to-panic.md" > ch09-03-to-panic-or-not-to-panic.md< / a >
< br >
commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1< / p >
< / blockquote >
< p > 那么,该如何决定何时应该< code > panic!< / code > 以及何时应该返回< code > Result< / code > 呢?如果代码 panic, 就没有恢复的可能。你可以选择对任何错误场景都调用< code > panic!< / code > ,不管是否有可能恢复,不过这样就你代替调用者决定了这是不可恢复的。选择返回< code > Result< / code > 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为< code > Err< / code > 是不可恢复的,所以他们也可能会调用< code > panic!< / code > 并将可恢复的错误变成了不可恢复的错误。因此返回< code > Result< / code > 是定义可能会失败的函数的一个好的默认选择。< / p >
< p > 有一些情况 panic 比返回< code > Result< / code > 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。< / p >
< h3 > 示例、代码原型和测试:非常适合 panic< / h3 >
< p > 当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似< code > unwrap< / code > 这样可能< code > panic!< / code > 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。< / p >
< p > 类似的,< code > unwrap< / code > 和< code > expect< / code > 方法在原型设计时非常方便,在你决定该如何处理错误之前。他们在代码中留下了明显的记号,以便你准备使程序变得更健壮时作为参考。< / p >
< p > 如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为< code > panic!< / code > 是测试如何被标记为失败的,调用< code > unwrap< / code > 或< code > expect< / code > 都是非常有道理的。< / p >
< h3 > 当你比编译器知道更多的情况< / h3 >
< p > 当你有一些其他的逻辑来确保< code > Result< / code > 会是< code > Ok< / code > 值的时候调用< code > unwrap< / code > 也是合适的,虽然编译器无法理解这种逻辑。仍然会有一个< code > Result< / code > 值等着你处理:总的来说你调用的任何操作都有失败的可能性,即便在特定情况下逻辑上是不可能的。如果通过人工检查代码来确保永远也不会出现< code > Err< / code > 值,那么调用< code > unwrap< / code > 也是完全可以接受的,这里是一个例子:< / p >
< pre > < code class = "language-rust" > use std::net::IpAddr;
let home = " 127.0.0.1" .parse::< IpAddr> ().unwrap();
< / code > < / pre >
< p > 我们通过解析一个硬编码的字符来创建一个< code > IpAddr< / code > 实例。可以看出< code > 127.0.0.1< / code > 是一个有效的 IP 地址,所以这里使用< code > unwrap< / code > 是没有问题的。然而,拥有一个硬编码的有效的字符串也不能改变< code > parse< / code > 方法的返回值类型:它仍然是一个< code > Result< / code > 值,而编译器仍然就好像还是有可能出现< code > Err< / code > 成员那样要求我们处理< code > Result< / code > ,因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就< strong > 确实< / strong > 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理< code > Result< / code > 了。< / p >
< h3 > 错误处理指导原则< / h3 >
< p > 在当有可能会导致有害状态的情况下建议使用< code > panic!< / code > ————在这里,有害状态是指当一些假设、保证、协议或不可变形被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值————外加如下几种情况:< / p >
< ul >
< li > 有害状态并不包含< strong > 预期< / strong > 会偶尔发生的错误< / li >
< li > 之后的代码的运行依赖于不再处于这种有害状态< / li >
< li > 当没有可行的手段来将有害状态信息编码进可用的类型中(并返回)的情况< / li >
< / ul >
< p > 如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是< code > panic!< / code > 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,< code > panic!< / code > 通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。< / p >
< p > 无论代码编写的多么好,当有害状态是预期会出现时,返回< code > Result< / code > 仍要比调用< code > panic!< / code > 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回< code > Result< / code > 来表明失败预期是可能的,这样将有害状态向上传播,这样调用者就可以决定该如何处理这个问题。使用< code > panic!< / code > 来处理这些情况就不是最好的选择。< / p >
< p > 当代码对值进行操作时,应该首先验证值是有效的,并在其无效时< code > panic!< / code > 。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会< code > panic!< / code > 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循< strong > 契约< / strong > ( < em > contracts< / em > ):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这这通常代表调用方的 bug, 而且这也不是那种你希望必须去处理的错误。事实上也没有合理的方式来恢复调用方的代码: 调用方的< strong > 程序员< / strong > 需要修复他的代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。< / p >
< p > 虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于< code > Option< / code > 的类型,而且程序期望它是< strong > 有值< / strong > 的而不是< strong > 空值< / strong > 。你的代码无需处理< code > Some< / code > 和< code > None< / code > 这两种情况,它只会有一种情况且绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像< code > u32< / code > 这样的无符号整型,也会确保它永远不为负。< / p >
< h3 > 创建自定义类型作为验证< / h3 >
< p > 让我们借用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型作为验证。回忆一下第二章的猜猜看游戏,它的代码请求用户猜测一个 1 到 100 之间的数字, 在将其与秘密数字做比较之前我们事实上从未验证用户的猜测是位于这两个数字之间的, 只保证它为正。在当前情况下, 其影响并不是很严重: “Too high”或“Too low”的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助, 例如当用户猜测一个超出范围的数字和输入字母时采取不同的行为。< / p >
< p > 一种实现方式是将猜测解析成< code > i32< / code > 而不仅仅是< code > u32< / code > ,来默许输入负数,接着检查数字是否在范围内:< / p >
< pre > < code class = "language-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
}
< / code > < / pre >
< p > < code > if< / code > 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用< code > continue< / code > 开始下一次循环,请求另一个猜测。< code > if< / code > 表达式之后,就可以在知道< code > guess< / code > 在 1 到 100 之间的情况下与秘密数字作比较了。< / p >
< p > 然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。< / p >
< p > 相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义< code > Guess< / code > 类型的方法,只有在< code > new< / code > 函数接收到 1 到 100 之间的值时才会创建< code > Guess< / code > 的实例:< / p >
< figure >
< pre > < code class = "language-rust" > struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 100 {
panic!(" Guess value must be between 1 and 100, got {}." , value);
}
Guess {
value: value,
}
}
pub fn value(& self) -> u32 {
self.value
}
}
< / code > < / pre >
< figcaption >
< p > Listing 9-8: A < code > Guess< / code > type that will only continue with values between 1 and
100< / p >
< / figcaption >
< / figure >
< p > 首先,我们定义了一个包含< code > u32< / code > 类型字段< code > value< / code > 的结构体< code > Guess< / code > 。这里是储存猜测值的地方。< / p >
< p > 接着在< code > Guess< / code > 上实现了一个叫做< code > new< / code > 的关联函数来创建< code > Guess< / code > 的实例。< code > new< / code > 定义为接收一个< code > u32< / code > 类型的参数< code > value< / code > 并返回一个< code > Guess< / code > 。< code > new< / code > 函数中代码的测试确保了其值是在 1 到 100 之间的。如果< code > value< / code > 没有通过测试则调用< code > panic!< / code > ,这会警告调用这个函数的程序员有一个需要修改的 bug, 因为创建一个< code > value< / code > 超出范围的< code > Guess< / code > 将会违反< code > Guess::new< / code > 所遵循的契约。< code > Guess::new< / code > 会出现 panic 的条件应该在其公有 API 文档中被提及;第十四章会涉及到在 API 文档中表明< code > panic!< / code > 可能性的相关规则。如果< code > value< / code > 通过了测试,我们新建一个< code > Guess< / code > ,其字段< code > value< / code > 将被设置为参数< code > value< / code > 的值,接着返回这个< code > Guess< / code > 。< / p >
< p > 接着,我们实现了一个借用了< code > self< / code > 的方法< code > value< / code > ,它没有任何其他参数并返回一个< code > u32< / code > 。这类方法有时被称为 < em > getter< / em > ,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为< code > Guess< / code > 结构体的< code > value< / code > 字段是私有的。私有的字段< code > value< / code > 是很重要的,这样使用< code > Guess< / code > 结构体的代码将不允许直接设置< code > value< / code > 的值:调用者< strong > 必须< / strong > 使用< code > Guess::new< / code > 方法来创建一个< code > Guess< / code > 的实例,这就确保了不会存在一个< code > value< / code > 没有通过< code > Guess::new< / code > 函数的条件检查的< code > Guess< / code > 。< / p >
< p > 如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个< code > Guess< / code > ,而不是< code > u32< / code > ,同时其函数体中也无需进行任何额外的检查。< / p >
< h2 > 总结< / h2 >
< p > Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。< code > panic!< / code > 宏代表一个程序无法处理的状态, 并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的< code > Result< / code > 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用< code > Result< / code > 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用< code > panic!< / code > 和< code > Result< / code > 将会使你的代码在面对无处不在的错误时显得更加可靠。< / p >
< p > 现在我们已经见识过了标准库中< code > Option< / code > 和< code > Result< / code > 泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如果在你的代码中利用他们。< / p >
< h1 > 泛型、trait 和生命周期< / h1 >
< blockquote >
< p > < a href = "https://github.com/rust-lang/book/blob/master/src/ch10-00-generics.md" > ch10-00-generics.md< / a >
< br >
commit b335da755592f286fd97a64d98f0ca3be6a59327< / p >
< / blockquote >
< p > 每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是< strong > 泛型< / strong > ( < em > generics< / em > )。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。< / p >
< p > 同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像< code > i32< / code > 或< code > String< / code > 这样的具体值。我们已经使用过第六章的< code > Option< T> < / code > ,第八章的< code > Vec< T> < / code > 和< code > HashMap< K, V> < / code > ,以及第九章的< code > Result< T, E> < / code > 这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法。< / p >
< p > 首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。< / p >
< p > 之后,我们讨论 < em > traits< / em > , 这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。< / p >
< p > 最后介绍< strong > 生命周期< / strong > ( < em > lifetimes< / em > ) , 它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值同时仍然使编译器能够检查这些引用的有效性。< / p >
< h2 > 提取函数来减少重复< / h2 >
< p > 在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。< / p >
< p > 考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-rust" > fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let mut largest = numbers[0];
for number in numbers {
if number > largest {
largest = number;
}
}
println!(" The largest number is {}" , largest);
# assert_eq!(largest, 100);
}
< / code > < / pre >
< figcaption >
< p > Listing 10-1: Code to find the largest number in a list of numbers< / p >
< / figcaption >
< / figure >
< p > 这段代码获取一个整型列表,存放在变量< code > numbers< / code > 中。它将列表的第一项放入了变量< code > largest< / code > 中。接着遍历了列表中的所有数字,如果当前值大于< code > largest< / code > 中储存的值,将< code > largest< / code > 替换为这个值。如果当前值小于目前为止的最大值,< code > largest< / code > 保持不变。当列表中所有值都被考虑到之后,< code > largest< / code > 将会是最大值,在这里也就是 100。< / p >
< p > 如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-rust" > fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let mut largest = numbers[0];
for number in numbers {
if number > largest {
largest = number;
}
}
println!(" The largest number is {}" , largest);
let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = numbers[0];
for number in numbers {
if number > largest {
largest = number;
}
}
println!(" The largest number is {}" , largest);
}
< / code > < / pre >
< figcaption >
< p > Listing 10-2: Code to find the largest number in < em > two< / em > lists of numbers< / p >
< / figcaption >
< / figure >
< p > 虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。< / p >
<!-- Are we safe assuming the reader will be familiar with the term
"abstraction" in this context, or do we want to give a brief definition? -->
<!-- Yes, our audience will be familiar with this term. /Carol -->
< p > 为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独。
立。< / p >
< p > 在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做< code > largest< / code > 的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-rust" > fn largest(list: & [i32]) -> i32 {
let mut largest = list[0];
for & item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(& numbers);
println!(" The largest number is {}" , result);
# assert_eq!(result, 100);
let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(& numbers);
println!(" The largest number is {}" , result);
# assert_eq!(result, 6000);
}
< / code > < / pre >
< figcaption >
< p > Listing 10-3: Abstracted code to find the largest number in two lists< / p >
< / figcaption >
< / figure >
< p > 这个函数有一个参数< code > list< / code > ,它代表会传递给函数的任何具体< code > i32< / code > 值的 slice。函数定义中的< code > list< / code > 代表任何< code > & [i32]< / code > 。当调用< code > largest< / code > 函数时,其代码实际上运行于我们传递的特定值上。< / p >
< p > 从列表 10-2 到列表 10-3 中涉及的机制经历了如下几步:< / p >
< ol >
< li > 我们注意到了重复代码。< / li >
< li > 我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。< / li >
< li > 我们将两个具体的存在重复代码的位置替换为了函数调用。< / li >
< / ol >
< p > 在不同的场景使用不同的方式泛型也可以利用相同的步骤来减少重复代码。与函数体中现在作用于一个抽象的< code > list< / code > 而不是具体值一样,使用泛型的代码也作用于抽象类型。支持泛型背后的概念与你已经了解的支持函数的概念是一样的,不过是实现方式不同。< / p >
< p > 如果我们有两个函数,一个寻找一个< code > i32< / code > 值的 slice 中的最大项而另一个寻找< code > char< / code > 值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待!< / p >
< h2 > 泛型数据类型< / h2 >
< blockquote >
< p > < a href = "https://github.com/rust-lang/book/blob/master/src/ch10-01-syntax.md" > ch10-01-syntax.md< / a >
< br >
commit 55d9e75ffec92e922273c997026bb10613a76578< / p >
< / blockquote >
< p > 泛型用于通常我们放置类型的位置,比如函数签名或结构体,允许我们创建可以代替许多具体数据类型的结构体定义。让我们看看如何使用泛型定义函数、结构体、枚举和方法,并且在本部分的结尾我们会讨论泛型代码的性能。< / p >
< h3 > 在函数定义中使用泛型< / h3 >
< p > 定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。< / p >
< p > 回到< code > largest< / code > 函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中< code > i32< / code > 最大值的函数。第二个函数寻找 slice 中< code > char< / code > 的最大值:< / p >
< figure >
< span class = "filename" > Filename: src/main.rs< / span >
< pre > < code class = "language-rust" > fn largest_i32(list: & [i32]) -> i32 {
let mut largest = list[0];
for & item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: & [char]) -> char {
let mut largest = list[0];
for & item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest_i32(& numbers);
println!(" The largest number is {}" , result);
# assert_eq!(result, 100);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest_char(& chars);
println!(" The largest char is {}" , result);
# assert_eq!(result, 'y');
}
< / code > < / pre >
< figcaption >
< p > Listing 10-4: Two functions that differ only in their names and the types in
their signatures< / p >
< / figcaption >
< / figure >
< p > 这里< code > largest_i32< / code > 和< code > largest_char< / code > 有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现。< / p >
< p > 为了参数化我们要定义的函数的签名中的类型< / p >
< / div >
< / div >