Rust 将错误组合成两个主要类别:可恢复错误(recoverable)和 不可恢复错误(unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
可恢复错误 Result
不可恢复错误panic!
Rust 有 panic!宏,当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。对应 panic 时的栈展开或终止
当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止(abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = ‘abort’,可以由展开切换为终止。 例如,如果你想要在release模式中 panic 时直接终止
[profile.release]panic = 'abort'
fn main() {panic!("crash and burn");}Compiling playground v0.0.1 (/playground)Finished dev [unoptimized + debuginfo] target(s) in 0.95sRunning `target/debug/playground`thread 'main' panicked at 'crash and burn', src/main.rs:2:5note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2:5 第二行第五个字符
使用 panic! 的 backtrace
最后几行提醒我们可以设置 RUSTBACKTRACE 环境变量来得到一个 backtrace。backtrace_ 是一个执行到目前位置所有被调用的函数的列表。
fn main() {let v = vec![1, 2, 3];v[99];}// error$ cargo runCompiling panic v0.1.0 (file:///projects/panic)Finished dev [unoptimized + debuginfo] target(s) in 0.27sRunning `target/debug/panic`thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10note: Run with `RUST_BACKTRACE=1` for a backtrace.
阅读 backtrace 的关键是从头开始读直到发现你编写的文件,这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。 为了获取带有这些信息的 backtrace,必须启用 debug 标识。
$ RUST_BACKTRACE=1 cargo runFinished dev [unoptimized + debuginfo] target(s) in 0.00sRunning `target/debug/panic`thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10stack backtrace:0: std::sys::unix::backtrace::tracing::imp::unwind_backtraceat libstd/sys/unix/backtrace/tracing/gcc_s.rs:491: std::sys_common::backtrace::printat libstd/sys_common/backtrace.rs:71at libstd/sys_common/backtrace.rs:592: std::panicking::default_hook::{{closure}}at libstd/panicking.rs:2113: std::panicking::default_hookat libstd/panicking.rs:2274: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::getat libstd/panicking.rs:4765: std::panicking::continue_panic_fmtat libstd/panicking.rs:3906: std::panicking::try::do_callat libstd/panicking.rs:3257: core::ptr::drop_in_placeat libcore/panicking.rs:778: core::ptr::drop_in_placeat libcore/panicking.rs:599: <usize as core::slice::SliceIndex<[T]>>::indexat libcore/slice/mod.rs:244810: core::slice::<impl core::ops::index::Index<I> for [T]>::indexat libcore/slice/mod.rs:231611: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::indexat liballoc/vec.rs:165312: panic::mainat src/main.rs:413: std::rt::lang_start::{{closure}}at libstd/rt.rs:7414: std::panicking::try::do_callat libstd/rt.rs:59at libstd/panicking.rs:31015: macho_symbol_searchat libpanic_unwind/lib.rs:10216: std::alloc::default_alloc_error_hookat libstd/panicking.rs:289at libstd/panic.rs:392at libstd/rt.rs:5817: std::rt::lang_startat libstd/rt.rs:7418: panic::main
可恢复的错误Result
T 和 E 是泛型类型参数 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。 让我们调用一个返回 Result 的函数,因为它可能会失败
enum Result<T, E> {Ok(T),Err(E),}
注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。 这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。 match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。
use std::fs::File;fn main() {let f = File::open("hello.txt");let f = match f {Ok(file) => file,Err(error) => {panic!("Problem opening the file: {:?}", error)},};}
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 是因为什么原因失败都会 panic!。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像上面代码 那样 panic!。
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),},};}// 较为复杂 后面会有讲解use std::fs::File;use std::io::ErrorKind;fn main() {let f = File::open("hello.txt").unwrap_or_else(|error| {if error.kind() == ErrorKind::NotFound {File::create("hello.txt").unwrap_or_else(|error| {panic!("Problem creating the file: {:?}", error);})} else {panic!("Problem opening the file: {:?}", error);}});}
失败时 panic 的简写:unwrap 和 expect
match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result还有另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:expect。使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。
use std::fs::File;fn main() {let f = File::open("hello.txt").unwrap();}// 如果调用这段代码时不存在 hello.txt 文件,我们将会看到一个 unwrap 调用 panic! 时提供的错误信息:thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {repr: Os { code: 2, message: "No such file or directory" } }',src/libcore/result.rs:906:4
expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 用来调用 panic! 的错误信息将会作为参数传递给 expect ,而不像unwrap 那样使用默认的 panic! 信息。 因为这个错误信息以我们指定的文本开始,Failed to open hello.txt,将会更容易找到代码中的错误信息来自何处。如果在多处使用 unwrap,则需要花更多的时间来分析到底是哪一个 unwrap 造成了 panic,因为所有的 unwrap 调用都打印相同的信息。
use std::fs::File;fn main() {let f = File::open("hello.txt").expect("Failed to open hello.txt");}thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
传播错误
当编写一个调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理,这被称为 传播(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");let mut f = match f {Ok(file) => file,Err(e) => return Err(e),};let mut s = String::new();// 隐形returnmatch f.read_to_string(&mut s) {Ok(_) => Ok(s),Err(e) => Err(e),}}
传播错误的简写:? 运算符
Result 值之后的 ? 被定义为与上面处理 Result 值的 match 表达式有着完全相同的工作方式。如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。 match 表达式与问号运算符所做的有一点不同:? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from 函数来定义如何将自身转换为返回的错误类型,? 运算符会自动处理这些转换。 在 ? 之后直接使用链式方法调用来进一步缩短代码
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();// 隐形returnmatch f.read_to_string(&mut s) {Ok(_) => Ok(s),Err(e) => Err(e),}}// 与上面代码相同功能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)}
Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。
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)}// brieflyuse std::io;use std::fs;fn read_username_from_file() -> Result<String, io::Error> {fs::read_to_string("hello.txt")}
? 运算符可被用于返回 Result 的函数
错误指出只能在返回 Result 或者其它实现了 std::ops::Try 的类型的函数中使用 ? 运算符。 当你期望在不返回 Result 的函数中调用其他返回 Result 的函数时使用 ? 的话,有两种方法修复这个问题。一种技巧是将函数返回值类型修改为 Result
use std::fs::File;fn main() {let f = File::open("hello.txt")?;}// errorerror[E0277]: the `?` operator can only be used in a function that returns`Result` or `Option` (or another type that implements `std::ops::Try`)--> src/main.rs:4:13|4 | let f = File::open("hello.txt")?;| ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in afunction that returns `()`|= help: the trait `std::ops::Try` is not implemented for `()`= note: required by `std::ops::Try::from_error`
Box
use std::error::Error;use std::fs::File;fn main() -> Result<(), Box<dyn Error>> {let f = File::open("hello.txt")?;Ok(())}
panic! 还是不 panic!
use std::net::IpAddr;// parse解析成IpAddr 返回Result类型let home: IpAddr = "127.0.0.1".parse().unwrap();
创建自定义类型进行有效性验证
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--}// 封装优化版pub struct Guess {value: i32,}impl Guess {pub fn new(value: i32) -> Guess {if value < 1 || value > 100 {panic!("Guess value must be between 1 and 100, got {}.", value);}Guess {value}}pub fn value(&self) -> i32 {self.value}}
