Skip to main content

错误处理

错误处理贯穿软件开发的始终, 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!, 还可以使用 unwrapexpect 自动在 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)
}

而上述代码还有更简化的写法(利用 fsread_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, 且使用 unwrapexpect 简化整个工作.