参考: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:5
note: 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
函数,它定义于标准库的From
trait 中,其用来将错误从一种类型转换为另一种类型。当?
运算符调用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