Rust的错误处理机制
任何语言都需要有错误处理机制,看到主流的语言清一色的try…catch…,Go和Rust又坐不住了,纷纷提出了自己的错误处理机制。Go的错误处理机制被骂的体无完肤,而Rust的错误处理却获赞不少。那么Rust的错误处理有什么独到之处呢,一起来学习一下吧。
Rust还会发生崩溃吗?
即使try…catch…,也有很多不能处理的错误而引发崩溃,是的,任何一种错误处理机制都不可能100%的捕获所有的错误。Rust的内存机制再出色、编译器再强大,也不能保证没有意外的发生。
以最基础的数组越界为例:
fn main() {
let a = [1, 2, 3, 4, 5];
println!("{}", a[5]);
}
编译器表现得很好,直接器报错:
error: this operation will panic at runtime
--> src\main.rs:3:20
|
3 | println!("{}", a[5]);
| ^^^^ index out of bounds: the len is 5 but the index is 5
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to previous error
很好,那我再换一个写法:
fn main() {
let a = [1, 2, 3, 4, 5];
let b = 4;
println!("{}", a[b+1]);
}
不负众望的编译器还是亮起来红灯:
error: this operation will panic at runtime
--> src\main.rs:4:20
|
4 | println!("{}", a[b+1]);
| ^^^^^^ index out of bounds: the len is 5 but the index is 5
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to previous error
看来我小瞧编译器了。那么,再提升一个难度呢?
use rand::Rng;
fn main() {
let a = [1, 2, 3, 4, 5];
let b = rand::thread_rng().gen_range(5, 999); // 生成一个随机数,这回编译器不知道b的值是多少了
println!("{}", a[b]);
}
果然,编译器不是万能的,这次编译通过了,但运行崩溃了:
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 30', src\main.rs:7:20
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
可见,强大如rust也没有办法摆脱崩溃的烦恼。
错误的分类
Rust将运行时错误分为两大类:
- 不可恢复的错误
- 可恢复的错误
对于大部分错误,并没有严重到无方可解的地步,这个时候,Rest采用Result枚举来解决(还记得吗?内置的枚举类型里有过一面之缘);然而,对于某些错误,一旦发生就是致命的,此时,Rust提供了一个叫做panic! 的宏,它会在不可恢复的错误发生之后,清理内存数据,然后停止程序的运行。
不可恢复的错误
使用随机数访问数组这个例子就发生了panic,Rust自动调用了panic! 宏,输出中提到了设置RUST_BACKTRACE=1
这个环境变量来查看调用堆栈的信息:
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 451', src\main.rs:7:20
stack backtrace:
0: backtrace::backtrace::dbghelp::trace
at C:\Users\VssAdministrator\.cargo\registry\src\github.com-1ecc6299db9ec823\backtrace-0.3.46\src\backtrace/dbghelp.rs:88
1: backtrace::backtrace::trace_unsynchronized
at C:\Users\VssAdministrator\.cargo\registry\src\github.com-1ecc6299db9ec823\backtrace-0.3.46\src\backtrace/mod.rs:66
2: std::sys_common::backtrace::_print_fmt
at src\libstd\sys_common/backtrace.rs:78
3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
at src\libstd\sys_common/backtrace.rs:59
4: core::fmt::write
at src\libcore\fmt/mod.rs:1076
5: std::io::Write::write_fmt
at src\libstd\io/mod.rs:1537
6: std::sys_common::backtrace::_print
at src\libstd\sys_common/backtrace.rs:62
7: std::sys_common::backtrace::print
at src\libstd\sys_common/backtrace.rs:49
8: std::panicking::default_hook::{{closure}}
at src\libstd/panicking.rs:198
9: std::panicking::default_hook
at src\libstd/panicking.rs:218
10: std::panicking::rust_panic_with_hook
at src\libstd/panicking.rs:486
11: rust_begin_unwind
at src\libstd/panicking.rs:388
12: core::panicking::panic_fmt
at src\libcore/panicking.rs:101
13: core::panicking::panic_bounds_check
at src\libcore/panicking.rs:73
14: guessing::main
at src/main.rs:7
15: std::rt::lang_start::{{closure}}
at C:\Users\zhang\.rustup\toolchains\stable-x86_64-pc-windows-gnu\lib/rustlib/src/rust\src\libstd/rt.rs:67
16: std::rt::lang_start_internal::{{closure}}
at src\libstd/rt.rs:52
17: std::panicking::try::do_call
at src\libstd/panicking.rs:297
18: std::panicking::try
at src\libstd/panicking.rs:274
19: std::panic::catch_unwind
at src\libstd/panic.rs:394
20: std::rt::lang_start_internal
at src\libstd/rt.rs:51
21: std::rt::lang_start
at C:\Users\zhang\.rustup\toolchains\stable-x86_64-pc-windows-gnu\lib/rustlib/src/rust\src\libstd/rt.rs:67
22: main
23: _tmainCRTStartup
24: mainCRTStartup
25: _report_error
26: _report_error
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
这个例子太于简单,调用堆栈都是Rust标准库的调用,没有多少有用的信息,如果真正的大型项目,相信还是有用的。
panic! 宏可可以被显式的调用:
fn main() {
panic!("我要挂了");
}
可恢复的错误
在前面见到Resut枚举时,用了一个例子,打开一个不存在的文件:
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => println!("open file success"),
Err(error) => println!("open file error:{:?}", error)
};
}
File::open返回的是Resutl枚举,Result枚举实现实现了多个方法,如unwrap
和expect
,它们可以用来简化match:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
在hello.txt不存在的情况下,运行时会有以下输出:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "系统找不
到指定的文件。" }', src\main.rs:4:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
unwrap
内部对open返回的Result进行了模式匹配,如果是Ok,则返回Ok的值,如果是Err,则直接调用panic!。我们希望能看到更多的信息,比如究竟是哪个文件找不到。这时,可以使用expect
。expect
允许在调用panic!时传入我们自己的信息:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("can not found file: 'hello.txt'");
}
这段代码输出:
thread 'main' panicked at 'can not found file: 'hello.txt': Os { code: 2, kind: NotFound, message: "系统找不到指定的文件
。" }', src\main.rs:4:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
从编程逻辑上,如果文件不存在,最好的方法是创建它。那么,打开文件失败就一定是文件不存在吗?当然不是,我们可以通过错误类型来判断:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => println!("open file success"),
Err(error) => match error.kind() {
ErrorKind::NotFound => println!("file not found"),
_ => println!("open file error:{:?}", error)
}
};
}
前面我们已经知道File::open返回一个Result枚举,我们通过match进行模式匹配。error.kind()也返回一个ErrorKind枚举,我们可以通过模式匹配嵌套的方式进行处理。我们已经可以判断是不是因为文件不存在而失败了,再进一步,就可以进行创建文件了。
但是问题来了,创建就一定能成功吗?显示也是不行的,因此,创建文件的函数也返回一个Result枚举:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => println!("open file success"),
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(file) => println!("create file success"),
Err(e) => println!("create file error: {}", e),
},
_ => println!("open file error:{:?}", error)
}
};
}
这段代码可以正常工作,可是三个match嵌套在一起看起来有些不太舒服。我们还可以使用unwrap
和expect
的变种,以unwrap
为例,它有4个变种:
-
unwrap_err:与
unwrap
,返回Err的值,当Result是Ok时panic -
unwrap_or(default):如果是Ok,则返回Ok的值,如果是Err返回给定的default
-
unwrap_or_default:如果是Ok,则返回Ok的值,如果是Err返回Ok相应类型的默认值
-
unwrap_or_else():传入一个闭包,如果是Ok,则返回Ok的值,如果是Err则执行这个闭包。我前面写过一篇关于闭包的文章,感兴趣的朋友可以看一下:https://blog.csdn.net/zhmh326/article/details/108443322。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|e| {
if e.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap()
} else {
panic!("open file error:{:?}", e);
}
});
}
传播错误
try…catch…有很好的传播特性,当异常发生时,如果不进行处理,它会传播到调用者那里,由调用者处理。Rust的错误处理同样可以实现错误的传播,不过,它不像try…catch…那样是自动的,而是需要将Result类型作为返回值一层层的返回,直到被处理。
比如我们封装一个从文件中读取数据的函数:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_file(path: &str) -> Result<String, io::Error> {
let mut f = match File::open(path) {
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),
}
}
fn main() {
println!("file content: {}", read_file("hello.txt").unwrap());
}
read_file函数返回了Result<String, io::Error>类型,在main调用时使用unwrap
对其进行处理。事实上,read_file并没有省略对Reuslt的处理,只是将它原样返回了出来。显然这种写法并不友好,这时,可以使用?
运算符:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
fn main() {
println!("file content: {}", read_file("hello.txt").unwrap());
}
第6行调用File::open后,接了一个?
,不再使用match匹配Result,这样的写法如上面的例子是完全等价的,即当Result为Ok时将文件句柄返回给f,当为Err时,函数直接返回Err。read_to_string也可以这么做,甚至,这两次调用可以使用链式表达:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_file(path: &str) -> Result<String, io::Error> {
let mut s = String::new();
File::open(path)?.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
println!("file content: {}", read_file("hello.txt").unwrap());
}
当出现错误时,这两个?
都有可能返回Err,没有错误时,返回Ok(s)。