rust语言基础学习: rust中的错误处理
2020-07-18
今天开始学习rust中错误处理的内容。
Rust中的错误可分为 可恢复错误(recoverable) 和 不可恢复错误(unrecoverable) 两个类别。
- 可恢复错误通常代表向用户报告错误和重试操作是合理的情况,例如未找到文件
- 不可恢复错误会导致程序崩溃,例如尝试访问超过数组结尾的位置
对比其他编程语言的错误处理:
- Java语言采用了异常机制的方式来处理,并没有明确区分可恢复错误和不可恢复错误。(ps: 虽然Java的异常给出了Throwable, Error, Exception, RuntimeException的继承关系体系,异常分为checked exception和uncheced exceppion,但在异常传播上采用的是通过栈回溯的方式一层层传递,直到出现捕获异常的地方。) Java语言这种异常处理方式的优点是简化了错误处理流程,但在运行时开销比使用返回值返回错误信息的方式要大很多。
- Go语言是明确区分可恢复错误(error)和不可恢复错误(panic)的。Go对可恢复错误采用了以函数返回错误值的形式,在函数返回时额外返回一个错误对象(error),这种方式的优点是错误处理的运行时开销小,缺点是返回的错误必须处理或者显式传播返回给上级调用,因此一个Go程序代码中会有大量的
if err!= nil {return err;}
。
Rust中没有异常,对于可恢复错误使用了类型Result<T, E>
,即函数返回的错误信息通过类型系统描述。对于不可恢复错误会panic!
。
1. Result<T, E>和可恢复错误 #
Result<T, E>
在rust中时一个枚举类型,其定义如下:
1#[derive(Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
2#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
3#[rustc_diagnostic_item = "result_type"]
4#[stable(feature = "rust1", since = "1.0.0")]
5pub enum Result<T, E> {
6 /// Contains the success value
7 #[lang = "Ok"]
8 #[stable(feature = "rust1", since = "1.0.0")]
9 Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
10
11 /// Contains the error value
12 #[lang = "Err"]
13 #[stable(feature = "rust1", since = "1.0.0")]
14 Err(#[stable(feature = "rust1", since = "1.0.0")] E),
15}
Result<T, E>
枚举有两个成员,Ok
和Err
。T
和E
是泛型参数,T
代表成功返回的Ok
成员中的数据类型。E
代表失败返回的Err
成员中的错误的类型。
有了这两个泛型参数,可以将Result枚举作为函数的返回值,用于各种场景下的可恢复错误的处理,当函数成功时返回Ok(T)
,失败时返回Err(E)
。
我们注意到Result的定义上面有一个must_use
的标注,rust的编译器会对must_use
标注的类型做特殊处理,如果该类型对应的值没有被显式使用,则就会有一个警告。
例如下面的代码。
例1:
1
2fn foo() -> Result<(), String> {
3 Ok(())
4}
5
6fn main() {
7 foo(); // unused `std::result::Result` that must be used
8
9}
例1的代码在调用foo
函数时,忽略了返回值Result,因为Result上有must_use
标注,所以Rust的编译器在编译时会报一个警告:
1warning: unused `Result` that must be used
2 --> src/main.rs:7:5
3 |
47 | foo(); // unused `std::result::Result` that must be used
5 | ^^^^^^
6 |
7 = note: `#[warn(unused_must_use)]` on by default
8 = note: this `Result` may be an `Err` variant, which should be handled
1.1 匹配不同的错误原因 #
在处理错误时,很多时候需要针对不同的错误原因进行不同的处理。
下面来学习一下rust标准库中的std::io
module中是如何设计错误处理的。
std::io
中定义了一个std::io::Result
:
1#[stable(feature = "rust1", since = "1.0.0")]
2pub type Result<T> = result::Result<T, Error>;
从io::Result
的定义可以看出,io::Result
实际上时result::Result<T, Error>
的别名。
io::Result
中的Err
成员类型是io::Error
。
io::Error
是一个结构体,它由一个kind()
方法签名是pub fn kind(&self) -> ErrorKind
,返回描述错误原因枚举ErrorKind
。
ErrorKind
枚举的成员是各种io错误原因,例如NotFound
, PermissionDenied
…
因此如果函数返回io::Result
,失败时返回的是io::Error
时,就可以调用kind方法,进一步匹配不同的错误原因进行不同处理。
例2:
1use std::fs::File;
2use std::io::ErrorKind;
3
4fn main() {
5 let f = File::open("hello.txt").unwrap_or_else(|err| {
6 match err.kind() {
7 ErrorKind::NotFound => File::create("hello.tx").unwrap_or_else(|error| {
8 panic!("Problem creating the file: {:?}", error);
9 }), // 匹配错误原因, 对于文件不存在的错误处理为创建文件
10 other_error_kind => panic!("Problem opening the file: {:?}", other_error_kind)
11 }
12 });
13 println!("{:?}", f);
14}
例2中还用到了Result的unwrap_or_else
方法,Result<T, E>
类型定义了很多辅助方法来处理各种情况。
除了unwrap_or_else
外,还有:
unwrap
方法: 如果Result的值是成员Ok
,unwrap
就返回Ok
的值;如果Result的值是成员Err
,unwrap
就会调用panic!
expect
方法: 与unwrap的使用方式一样,允许我们传参指定panic!
的信息
1.2 使用?
操作符传播错误
#
经常在编写一个函数实现时会调用另一个返回Result<T, E>
的函数,除了在这个函数中处理错误之外,还可以选择将错误传播到上游调用者,这就是传播错误。
rust还提供了强大的?
操作符,如果我们只想要传播错误,而不想直接处理,可以使用?
操作符。
例3:
1use std::io;
2use std::io::Read;
3use std::fs::File;
4
5fn read_username_from_file() -> Result<String, io::Error> {
6 let mut f = File::open("hello.txt")?;
7 let mut s = String::new();
8 f.read_to_string(&mut s)?;
9 Ok(s)
10}
例3代码中第6行的?
操作符会被展开成类似下面的代码:
1match result {
2 Ok(v) => v,
3 Err(e) => Err(e.into())
4}
对比一下Go语言里if err!= nil {return err;}
,在rust中传播错误是不是比较爽。
2. panic! 和不可恢复错误 #
rust提供了一个panic!
宏,当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。
出现这种情况的场景通常是检测到一些类型的 bug,而且Rust程序员并不清楚该如何处理它时,对应的是不可恢复错误。
panic!表示不可恢复的错误,希望程序马上终止运行并得到崩溃信息。
可以在需要时在我们自己的代码中使用panic!
,例如panic!("crash and burn");
。
rust标准库还提供了catch_unwind()
,可以把panic的调用栈回溯到catch_unwind的时候,作用有点类似于Go语言中的recover
。
例4:
1use std::panic;
2fn main() {
3 let result = panic::catch_unwind(|| {
4 panic!("crash");
5 });
6 if result.is_err() {
7 println!("panic reover: {:#?}", result);
8 }
9 println!("exit ok!");
10}
例4的代码运行结果如下:
1thread 'main' panicked at 'crash', src/main.rs:4:9
2note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
3panic reover: Err(
4 Any { .. },
5)
6exit ok!
最后,需要注意catch_unwind
的使用场景可对标go语言中的recover
,不能到处滥用,因为rust的catch_unwind
和go的recove
一样,不是Java里的异常处理。