错误处理
错误处理贯穿软件开发的始终, Rust 提供了许多功能帮助在出现错误时进行对应处理.
Rust 将错误分为两大类:
- 可恢复的(recoverable): 比如"文件不存在"这种错误, 我们一般的处理就是能够将错误上报.
- 不可恢复的(unrecoverable): 不可恢复的错误一般都意味着代码中出现 bug, 比如数组越界访问, 因此直接终止程序是比较合理的选择.
其他语言中可能将这两类错误都统一为 exception(异常). 在 Rust 中没有异常的概念, 而是提供 Result<T,E>
类型对 recoverable 错误进行处理, 另外提供 panic!
宏对 unrecoverable 错误进行处理.
panic!
宏的使用
在代码中有两种方式触发 panic:
- 比如出现数据越界访问时自动触发
- 手动调用
panic!
宏触发 panic
默认情下, panic 时, Rust 程序会打印错误信息, unwind, 清理栈空间, 然后退出.
如果在环境变量中进行设置, 则可以让 Rust 在 panic 时打印当 panic 发生时的调用栈信息, 这样可以非常容易地定位问题所在.
Panic 宏的调用非常简单:
fn main() {
panic!("crash and burn");
}
Rust Panic 时的栈 Unwinding 和忽略
默认情况下, 当 panic 发生时, 程序会自动开始 unwinding
, 即 Rust 会主动推出整个调用栈中的每一帧, 在期间清理每帧中的数据.
但这种退栈和清理一般都有非常大的工作量, 因此 Rust 允许开发者指定不进行 unwinding
, 而是直接 aborting
, 即直接结束程序而不进行清理作业. 这样清理工作就由操作系统接管.
如果想要最终打包的二进制文件尽可能小, 则可以将 panic 处理时的默认 unwinding 行为设置为 aborting 行为.(在 [profile]
中添加 panic = 'abort'
), 比如:
[profile.release]
panic = 'abort'
使用 panic!
的崩溃栈信息
比如下面的越界访问代码:
fn main() {
let v = vec![1, 2, 3];
v[99];
}
在 C 中类似的代码会有无可预料的行为, 因为访问的内存地址是不确定它什么权限/什么数据的, 这种访问在 C 中又叫缓冲区溢出, 攻击者可以利用这一点对软件/系统进行攻击, 或读取到他们并不应该读取到的数据.
在 Rust 中如果尝试越界访问, 则会直接 panic.
默认情况下 Rust 不会打印崩溃栈信息:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
可以看到, 上面提示可以设置 RUST_BACKTRACE
从而打印崩溃栈信息:
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
1: core::panicking::panic_fmt
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
2: core::panicking::panic_bounds_check
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15
5: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982
6: panic::main
at ./src/main.rs:4
7: core::ops::function::FnOnce::call_once
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
可以看到第6个栈帧中有 panic 发生, 并且指向了对应 的代码行.
要获取上面的完整信息, 二进制文件需要包含 debug symbol, 如果是 release 编译时, 则不会携带.
使用 Result
类型表示可恢复 Error
Result
类型为:
enum Result<T, E> {
Ok(T),
Err(E),
}
可以使用 match
处理 Ok 和 Err 的情况:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
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);
}
},
};
}
如果不使用 match
, 还可以使用 unwrap_or_else
进行:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = 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
自动在 error 时触发 panic. 二者区别是 expect
可以提供自定义的消息字符串.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
// 或
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Rust 生产环境中更多使用的是 expect
, 因为 可以提供更多的上下文信息, 便于定位和修复错误.
Result
出错时的传递
如果在某个函数中不想处理出现的错误, 则可以将错误传递出去.
比如:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
上面的代码由于使用 match, 导致非常冗长, Rust 提供了更简化的操作符 ?
进行错误传递, 下面的代码和上面等效, 但短小很多:
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
在 Result
类型上使用 ?
操作符后, 如果 Ok 则向下继续执行, 否则直接 return 错误. 注意 ?
操作符只能在返回值为 Result
的函数中使用.
如果合理利用 ?
操作符, 可以极大减少代码量, 比如可以再改写上述代码为:
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
而上述代码还有更简化的写法(利用 fs
的 read_to_string
函数):
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
Rust 的 main 函数同样可以有 Result
类型的返回值:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
这样写之后, 如果 main 函数中没有遇到 Err
, 则返回 0, 否则返回非零值.
另外 main
函数可以返回任何实现了 Termination
协议的类型,
关于 panic!
和 Result
的选择
在例子工程或代码原型, 测试代码中, 更好地选择是直接在 error 时调用 panic
, 且使用 unwrap
或 expect
简化整个工作.