Rust 错误处理
概述
大多数语言并不区分可恢复错误和不可恢复错误,并采用类似异常这样方式统一处理他们。
Rust 没有异常。相反,它有 Result<T, E> 类型,用于处理可恢复的错误,还有 panic! 宏,在程序遇到不可恢复的错误时停止执行。
panic!
处理不可恢复的错误
- 当执行这个宏时,
程序会打印出一个错误信息
展开(unwind)并清理栈(Stack)
然后退出程序。
当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止(abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。
panic!可能直接的宏调用,另一个可能是我们代码中的 bug 引起的别的库中 panic!
//1
fn main() {
panic!("crash and burn");
}
//2
fn main() {
let v = vec![1, 2, 3];
v[99];
}
Result
处理可恢复的错误
这一部分比较有意思
- Result枚举
enum Result<T, E> {
Ok(T),
Err(E),
}
里面有两个变体,两个变体都关联数据,T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。
- 使用match表达式
use std::fs::File;
fn main(){
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。match 之后,我们可以利用这个文件句柄来进行读写。
match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。
- 更复杂一点
我们希望对 File::open 不同的错误原因采取不同的行为
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!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
},
};
}
error的类型是io::Error,是标注库中的结构体,这其中有一个kind方法,得到Errorkind类型值,也是一个枚举。
match 中检查的条件是 error.kind() 的返回值是否为 ErrorKind的 NotFound 成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内层 match 语句。当文件不能被打开,会打印出一个不同的错误信息。外层 match 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。
我们可以发现match确实强大,但是也非常基础,多层嵌套的match会使得结构不太清晰,为此我们可以使用闭包(closure)
- 闭包
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = 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);
}
});
}
- unwrap
match的快捷方法
作用::如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!。
use std::fs::File;
fn main() {
// let f = File::open("hello.txt");
// let f = match f {
// Ok(file) => file,
// Err(error) => panic!("Problem opening the file: {:?}", //error),}
let f = File::open("hello.txt").unwrap();
}
- expect
相比于unwrap,expect可以指定错误信息
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
Result 传播错误
将错误返回给调用者
当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。
use std::fs::File;
use std::io::{self, Read};
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),
}
}
注意函数的返回值:Result<String, io::Error>,这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open 函数和 read_to_string 方法。
- ?运算符
简化传播错误的方式
实现与刚才相同的操作
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。
被?应用的错误,会隐式的被from函数处理,这个from用于不同错误类型之间的转换
当?调用from函数时,它所接收的错误类型会被转化为当前函数返回类型定义的错误类型。
- ?链式调用
use std::fs::File;
use std::io;
use std::io::Read;
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)
}
- ?与主函数
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
Box 类型是一个 trait 对象(trait object),可以将 Box 理解为 “任何类型的错误”。
当 main 函数返回 Result<(), E>,如果 main 返回 Ok(()) 可执行程序会以 0 值退出,而如果 main 返回 Err 值则会以非零值退出;
参考:《Rust程序设计语言》