Rust 中的错误处理
异常/错误处理 1 被称作是**“第四流程控制语句”**,在现代程序设计中发挥着越来越重要的作用。一般的语言喜欢使用 try...catch...
语句捕捉异常,这样做看似很符合逻辑,却很难将代码写得优雅:如果在异常抛出处处理,容易破坏业务逻辑的完整性,不优雅;如果将所有的异常放在一起统一处理,又容易出现忘记处理的情况。所以如何处理异常,何时处理异常是一件非常考验程序员经验的事情。但 Rust 就不同了。
Rust 中的错误处理方式是我见过最优雅的。Rust 中并没有异常,而主要是使用一种名为 Result
(实际上是一种枚举) 的概念处理错误。它的好处就在于不会影响业务逻辑,因为它本身就是业务逻辑的一部分!
Rust 中,有两种错误:
- 可恢复错误 (recoverable)
- 不可恢复错误 (unrecoverable)
大部分语言不区分这两类错误,并使用相似的逻辑处理他们。Rust 使用 Result
和 panic!
处理可恢复错误,在遇到不可恢复错误时直接使用 panic!
停止程序执行。
先来解释一下 panic!
。
1 异常和错误是有区别的,这里只比较他们的相似性,不多做区分。
panic!🔗
panic!
处理错误时会采用展开 (unwinding) 和终止 (abort) 两种策略处理栈上的数据。
展开的意思就是说 Rust 会回溯栈并一层一层的清理它遇到的每一个函数的数据,就像这样:
Finished dev [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 99', /checkout/src/liballoc/vec.rs:1555:10
stack backtrace:
0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
1: std::sys_common::backtrace::_print
at /checkout/src/libstd/sys_common/backtrace.rs:71
2: std::panicking::default_hook::{{closure}}
at /checkout/src/libstd/sys_common/backtrace.rs:60
at /checkout/src/libstd/panicking.rs:381
3: std::panicking::default_hook
at /checkout/src/libstd/panicking.rs:397
4: std::panicking::rust_panic_with_hook
at /checkout/src/libstd/panicking.rs:611
5: std::panicking::begin_panic
at /checkout/src/libstd/panicking.rs:572
6: std::panicking::begin_panic_fmt
at /checkout/src/libstd/panicking.rs:522
7: rust_begin_unwind
at /checkout/src/libstd/panicking.rs:498
8: core::panicking::panic_fmt
at /checkout/src/libcore/panicking.rs:71
9: core::panicking::panic_bounds_check
at /checkout/src/libcore/panicking.rs:58
10: <alloc::vec::Vec<T> as core::ops::index::Index<usize>>::index
at /checkout/src/liballoc/vec.rs:1555
11: panic::main
at src/main.rs:4
12: __rust_maybe_catch_panic
at /checkout/src/libpanic_unwind/lib.rs:99
13: std::rt::lang_start
at /checkout/src/libstd/panicking.rs:459
at /checkout/src/libstd/panic.rs:361
at /checkout/src/libstd/rt.rs:61
14: main
15: __libc_start_main
16: <unknown>
是不是很像异常?Rust 默认使用展开的策略清理内存,不过展开堆栈需要额外记录堆栈信息,这会对二进制程序的大小以及执行效率 (微乎其微) 产生影响。
另一种选择是直接终止,这会不清理数据就直接退出。剩下的一堆烂摊子扔给操作系统来处理。
展开堆栈时可以用 std::panic::catch_unwind
捕获 panic
抛出的堆栈,但是极不推荐用这种方法来处理错误。catch_unwind
一般是用来在多线程程序里面在将挂掉的线程 catch 住,防止一个线程挂掉导致整个进程崩掉,或者是通过外部函数接口 (FFI) 与 C 交互时将堆栈信息兜住防止 C 程序看到堆栈不知道如何处理。另外并不是所有程序都能用 catch_unwind
捕捉,有的嵌入式平台上的程序受限于二进制文件大小的限制,panic
没有使用展开,而是使用终止的方式退出程序,这就没法兜得住了。
Result🔗
就下来介绍一种更常用也是我最喜欢的一种错误处理方式:Result
类型。
Result
的本质是一个枚举类型。首先我们需要了解一下什么是枚举。
Result 与枚举🔗
枚举是 Rust 特有的一种类型,与 F#、OCaml 和 Haskell 这样的函数式编程语言中的代数数据类型 (algebraic data types) 最为相似。
Result
是 Rust 自带的一个已经定义好了的枚举。Result
中定义了两个成员:
enum Result<T, E> {
Ok(T),
Err(E),
}
OK
表示成功的情况,Err
表示出错的情况 (T
和 E
分别表示 OK
或 Err
中数据的类型)。如果你调用了一个可能会出错的函数,那么函数将会返回一个 Result
枚举。一个枚举实例表示枚举类型定义的成员中的任意一项,反映到 Result
上就是 OK
或 Err
。也就是说函数返回的 Result
既可能是一个包含 OK
的 Result
,也可能是一个包含 Err
的 Result
,这有点类似于“薛定谔的猫”——处于生与死的叠加态——可以说 Result
处于 OK
与 Err
的“叠加态”。
那么这又有什么好处呢?
Result
的优越性就在于:无论程序出错与否,**函数返回的值的类型始相同的!**函数的调用者可以采用相同的方式行云流水般的处理 Result
。也就是说,无论成功与否,函数的调用者都要处理 Result
,错误处理成为了业务逻辑的一部分。
处理错误🔗
我们已经成功的获得了从调用的函数手里返回的 Result
,我们并不知道执行到底有没有成功。这时我们就应该开始处理错误了。
unwrap 与 expect🔗
我们先看这么一个例子:
use std::fs::File;
fn main() {
let f1 = File::open("hello.txt").unwrap();
let f2 = File::open("hello.txt").expect("Failed to open hello.txt");
}
代码中,我们使用 File::open
来读打开一个名为 hello.txt
的文件。函数 File::open
将会返回一个 io::Result
。Rust 标准库中有很多叫做 Result
的类型,io::Result
是其中之一,并且 io::Result
的实例拥有 unwrap
方法和 expect
方法。 如果程序执行成功 unwrap
或 expect
方法会将正确的值取出来,如果出错就直接让程序挂掉。其中 unwrap
方法在挂掉时会打印出标准库内置的错误信息,而 exprct
则让我们可以自己定义一个字符串在程序挂掉时显示。
unwrap
/expect
并不是一个好的错误处理方式。 **unwrap
和 expect
只会捕捉错误,然后终止程序,并没有真正的处理错误。**所以 unwrap
和 expect
一般只是用于原型设计。
match🔗
使用 match
来处理错误是最常用,也是最推崇的做法。还是刚刚的那个例子,如果用 match
来写的话:
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)
},
};
}
可以把 match
表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会通过 match
的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。
在本例中,f
就是那个硬币,OK
和 Err
就是那个孔;match
会把 f
(io::Result
) 实例中值的类型与 OK
、Err
做匹配,如果匹配到了合适的类型就执行 =>
后的表达式。当结果是 Ok
时,返回 Ok
成员中的 file
值,然后将这个文件句柄赋值给变量 f
。当结果是 Err
时,我们选择调用 panic!
宏。
匹配不同的错误🔗
上文的代码不管 File::open
是因为什么原因失败都会 panic!
。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open
因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open
因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望 panic!
。
只需要增加一个 match
就可以了:
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!("Tried to create file but there was a problem: {:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
如果觉得太麻烦了,还可以使用闭包:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").map_err(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Tried to create file but there was a problem: {:?}", error);
})
} else {
panic!("There was a problem opening the file: {:?}", error);
}
});
}
传播错误🔗
Rust 允许程序像别的语言处理“异常”一样的将错误扔给更上一层的调用者,这被称为传播 (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();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
在上面的代码中,程序会尝试打开一个名为 hello.txt
的文件,如果失败将会提前返回一个包含 Err
的 Result
;如果成功,则将会尝试读取 hello.txt
中的信息,如果成功则会返回包含储存了被读取信息的 String
的 Result
,否则返回包含 Err
的 Result
。
也可以使用 ?
简写:
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)
}
这段代码与上一段代码的作用相同:打开文件 -> 读取文件;但是这一段代码更简洁。File::open
调用结尾的 ?
将会返回 Ok
中的值,也就是文件 hello.txt
的文件句柄。如果出现了错误,?
会提早返回整个函数并将一些 Err
值传播给调用者。同理也适用于 read_to_string
调用结尾的 ?
。
match
表达式与问号运算符所做的有一点不同:?
所使用的错误值被传递给了 from
函数,它定义于标准库的 From
trait 中,其用来将错误从一种类型转换为另一种类型。当 ?
调用 from
函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from
函数来定义如将其转换为返回的错误类型,?
会自动处理这些转换。
?
只能被用于返回 Result
的函数