参考:https://kaisery.github.io/trpl-zh-cn/ch09-00-error-handling.html
Rust 将错误组合成两个主要类别:可恢复错误 (recoverable )和 不可恢复错误 (unrecoverable )。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是,有可恢复错误 Result<T, E> ,和不可恢复(遇到错误时停止程序执行)错误 panic!。
panic!:不可恢复错误
panic!宏:
- 当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug,而且程序员并不清楚该如何处理它。
当出现 panic 时,程序默认会开始 展开 (unwinding ),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止 (abort ),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的
[profile]部分增加panic = 'abort',可以由展开切换为终止。例如,如果你想要在release模式中 panic 时直接终止:[profile.release]panic = 'abort'
panic 的报错信息会提示程序出错的位置:
thread 'main' panicked at 'crash and burn', src/main.rs:2:5note: Run with `RUST_BACKTRACE=1` for a backtrace.
src/main.rs:2:5的2:5指的就是src/main.rs文件 第 2 行 第 5 个字符的地方导致了 panic。panic!可能会出现在我们的代码所调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的panic!宏调用(即最原始出错的地方),而不是我们代码中最终导致panic!的那一行。把
RUST_BACKTRACE环境变量设置为任何不是 0 的值来获取完整 backtrace,此外还必须启用 debug 标识。当不使用--release参数运行 cargo build 或 cargo run 时 debug 标识会默认启用。
Result:可恢复错误Result是具有Ok()和Err()两个成员的枚举体,成员类型是泛型参数的。因为Result有这些泛型类型参数,我们可以将Result类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。enum Result<T, E> {Ok(T),Err(E),}
与
Option枚举一样,Result枚举和其成员也被导入到了 prelude 中,所以就不需要在match分支中的Ok和Err之前指定Result::。match模式匹配Ok和Err经典例子:打开文本文件
use std::fs::File;fn main() {let f = File::open("hello.txt"); // 返回 Result 类型let f = match f {Ok(file) => file,Err(error) => {panic!("Problem opening the file: {:?}", error)},};}
如果文件不存在,则得到
Err,并 panic ,打印如下输出:thread 'main' panicked at 'Problem opening the file: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
通过查阅文档,可以发现
File::open返回的Err成员中的值类型是enum: std::io::ErrorKind。据此我们可以区分更详细的错误来做不同的处理,比如“文件不存在”那么就创建它。在其他语言中也是类似的思路,而在 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(error) => match error.kind() {ErrorKind::NotFound => match File::create("hello.txt") {Ok(fc) => fc,Err(e) => panic!("Problem creating the file: {:?}", e),},other_error => panic!("Problem opening the file: {:?}", other_error),},};}
.unwarp()和.except("...")就像处理
Option类型那样, 有很多方式来消除处理Result时的多层match嵌套。比如遇到Err直接 panic:.unwrap()或.expect("desciption")方法。
如果Result值是成员Ok,unwrap会返回Ok中的值;如果Result是成员Err,unwrap会为我们调用panic!。
而except在unwrap处理Err的基础上支持添加 panic 时打印的描述。即代替了这种形式的match:match expr {Ok(item) => item,Err(err) => panic!("description"),}
因此 “上面打开文件的例子” 就可以用一行替代:
let f = File::open("hello.txt").expect("Failed to open hello.txt");传播错误 和
?运算符当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。后者做法被称为 传播 (propagating )错误,这样能更好的控制代码调用,因为调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
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");// 可能遇到的错误 1:打开文件时出错let mut f = match f {Ok(file) => file,Err(e) => return Err(e),};let mut s = String::new();// 可能遇到的错误 2:读取时出错match f.read_to_string(&mut s) {Ok(_) => Ok(s),Err(e) => Err(e),}}
调用这个函数的代码最终会得到一个包含用户名的
Ok值,或者一个包含io::Error的Err值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个Err值,他们可能会选择panic!并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播(向外告知),让他们选择合适的处理方法。Result值之后使用?表示:如果
Result的值是Ok,这个表达式将会返回Ok中的值而程序将继续执行;- 如果值是
Err,Err中的值将作为整个函数的返回值,就好像使用了return关键字一样,这样错误值就被传播给了调用者。
“上述代码” 的转换成 ? 的写法:
use std::io;use std::io::Read;use std::fs::File;fn read_username_from_file() -> Result<String, io::Error> {let mut f = File::open("hello.txt")?;let mut s = String::new();f.read_to_string(&mut s)?;Ok(s)}
很明显,? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码:
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)}
将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。所以这个例子最终可以用一行语句达到相同的效果:
use std::io;use std::fs;fn read_username_from_file() -> Result<String, io::Error> {fs::read_to_string("hello.txt")}
? 与 match 表达式不同的地方:
只能在返回
Result或者其它实现了std::ops::Try的类型的函数中使用?运算符。main函数是特殊的,其必须返回什么类型是有限制的。main函数的一个有效的返回值是(),另一个有效的返回值是Result<T, E>。use std::error::Error;use std::fs::File;fn main() -> Result<(), Box<dyn Error>> {let f = File::open("hello.txt")?;Ok(())}
Box<dyn Error>被称为 “trait 对象”(“trait object”)。#todo: trait# 目前可以理解Box<dyn Error>为使用?时main允许返回的 “任何类型的错误”。?运算符所使用的错误值被传递给了from函数,它定义于标准库的Fromtrait 中,其用来将错误从一种类型转换为另一种类型。当?运算符调用from函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了from函数来定义如何将自身转换为返回的错误类型,?运算符会自动处理这些转换。
是否应该
panic!panic!宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的Result枚举代表操作可能会在一种可以恢复的情况下失败。可以使用Result来告诉代码调用者他需要处理潜在的成功或失败。
返回Result是定义可能会失败的函数的一个好的默认选择。你可以选择对任何错误场景都调用panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回Result值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为Err是不可恢复的,所以他们也可能会调用panic!并将可恢复的错误变成了不可恢复的错误。
示例、代码原型和测试都非常适合 panic ,当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似 unwrap 这样可能 panic! 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。类似地,在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 panic! 会将测试标记为失败,此时调用 unwrap 或 expect 是恰当的。
当我们比编译器知道更多的情况:当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 Result 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 Err 值,那么调用 unwrap 也是完全可以接受的
参考:https://kaisery.github.io/trpl-zh-cn/ch09-03-to-panic-or-not-to-panic.html
