Rust 中将错误分成了正常的业务错误 Resut 以及崩溃 panic 处理。
简介
对于错误处理,主流分成了两种:A) 异常机制,例如Java、Python等;B) 返回错误,例如GoLang、Rust等。首先看下,通过标准库如何更加优雅的处理异常,总共有如下的几种方式:
Option<T>
包括了Some(value)
、None
。Result<T>
包括了Ok(value)
、Err(err)
。panic!
应对非预期的异常,会导致线程退出,同时会做栈展开 (需要设置RUST_BACKTRACE=1
环境变量),并自动调用 Drop 方法完成资源释放。abort!
强制异常退出,不返回任何值,不做栈展开,也不会调用相应的析构函数。
Option
在 Rust 中没有类似 null
的空值概念,取而代之的是一个 Option 枚举,用来表达存在性,包含在 prelude
中,可以直接使用,包括了成员 Some
和 None
也不需要 Option
前缀。
enum Option<T> {
None,
Some(T),
}
其它语言中,在调用一个接口后需要判断返回值是否有效,例如如下示例。
User user = model.getUser();
if (user != null) {
return;
}
System.out.println("Hello " + user.getName());
只是开发时很容易忘记判断,而且这种逻辑也稍微有些啰嗦,通过 Option 就可以解决这个问题。
fn main() {
let file = "README.md";
let ext = match file.find('.') {
None => None,
Some(i) => Some(&file[i+1..]),
};
if let Some(data) = ext {
println!("{:?}", data);
}
}
就是通过编译器强制用户显式处理存在和不存在两种结果,其中的 let Some(x) = xx
是一个 bool
表达式,但是不能单独使用。
上述的方式还是有些复杂,不过好在 Rust 提供了很多算子,这样就可以通过如下方式简化。
fn main() {
let file = "README.md";
file.find('.').map(|i| println!("{:?}", &file[i+1..]));
}
常用函数
为了方便处理 Option 返回结果,同时提供了很多常用的函数,可以参考 Enum Option 中的介绍。例如,如果只是测试那么可以使用 unwrap()
方法。
fn main() {
let file = "README.md";
let offset = file.find('.').unwrap();
println!("{:?}", &file[offset+1..]);
}
其中的 unwrap()
在 Option
中实现,如果正常则获取对应的值,否则执行 panic()
,所以不太适合线上,可以通过 unwrap_or()
在为 None
时返回一个默认值,还有一个 unwrap_or_else()
通过函数处理为 None
情况。
另外,需要注意,对于 unwrap_or()
无论返回是否为 None
其中的函数都会被调用,也就是 eagerly evaluated
,而 unwrap_or_else()
则只有当值是 None
时才会调用,也就是 lazily evaluated
。
还有个与 unwrap()
类似的 expect()
函数,区别是后者可以指定报错信息,而前者则是默认的值。
let f = File::open("hello.txt").expect("Failed to open hello.txt");
另外,处理 map()
函数,还可以参考 and_then()
算子。
Result
在 std::result 模块中提供了 Result
枚举类型,可以看做 Option
的加强版,同时还可以返回错误信息,通常用来返回处理结果,其定义如下。
enum Result<T, E> {
Ok(T), // 操作成功,并包含操作返回的值
Err(E), // 操作失败,同时会包含具体失败的原因
}
如下是一个示例。
use std::str;
use std::result::Result;
fn is_fifty(num: u32) -> Result<u32, &'static str> {
if num == 50 {
Ok(num)
} else {
Err("It didn't work")
}
}
fn main() {
let num = match is_fifty(50) {
Result::Ok(o) => o,
Result::Err(..) => {
panic!("invalid result")
},
};
println!("got value {}", num);
}
还可以通过 type Option<T> = Result<T, ()>;
定义别名,其中的 ()
被称为 unit
或者空元组 empty tuple
,该 ()
类型只对应一个值 ()
,也就是即使类型也是值。
也可以只处理报错的情况,示例如下。
fn main() {
let val = "20";
if let Err(e) = val.parse::<i32>().map(|n| 2 * n) {
println!("Error: {:?}", e);
}
}
与 Option 类似,同样包含了 unwrap()
、unwrap_or()
、map()
、and_then()
等算子,同时还有针对错误类型的算子,例如 map_err()
、or_else()
等,更多函数可以参考 Enum Result 中的介绍。
fn main() {
let val = "20";
match val.parse::<i32>().map(|n| 2 * n) {
Ok(n) => println!("got value {}", n),
Err(err) => println!("Error: {:?}", err),
}
}
别名
在很多标准库中,通常是以 Result<i32>
这种方式使用,最常用的就是 io::Result
了,通常为 io::Result<T>
,其实就是以别名的方式将错误类型固定下来了,例如如下定义。
use std::num::ParseIntError;
use std::result;
type Result<T> = result::Result<T, ParseIntError>;
fn double(num: &str) -> Result<i32> {
num.parse::<i32>().map(|n| 2 * n)
}
fn main() {
match double("20") {
Ok(n) => println!("got value {}", n),
Err(err) => println!("Error: {:?}", err),
}
}
unwrap
如下代码会抛出异常,默认没有打印调用栈,如果要打印,需要添加 RUST_BACKTRACE=1
环境变量。
fn guess(n: i32) -> bool {
if n < 1 || n > 10 {
panic!("Invalid number: {}", n);
}
n == 5
}
fn main() {
guess(11);
}
而 unwrap
的意思就是,根据计算的结果,如果是错误则会直接调用 panic
,而且,最常用的 Option
和 Result
已经实现了该接口。虽然出现异常时会导致程序 panic 退出,但是在示例代码中会大大简化处理逻辑。
另外,还可以通过 ?
问号来简化 Option
和 Result
的处理,如果有值会将值提取出来,否则返回,此时函数也要求返回 Option
或者 Result
类型。
自定义
很多的库中需要自定义错误,大部分场景需要调用底层的接口,而底层可能对应了不同的错误类型,所以需要同时处理不同的类型的错误转换,例如如下的示例。
#[derive(Debug)]
pub enum Error {
IO(String),
// 某个错误里已经包含了详细的错误信息以及上下文
Parse{source: url::ParserError},
}
// 应用标准接口
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::IO(msg) => write!(f, "io error: {msg}"),
}
}
}
// 处理通过From作隐式转换,还可以通过 map_err 实现
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IO(e.to_string())
}
}
// 覆盖原有的Result类型,提供默认的Error信息,也可以在某些地方指定具体错误
type Result<T, E = Error> = std::result::Result<T, E>;
标准库
标准库提供了 std::error::Error
接口,其同时继承了 Debug
Display
接口,最常见的就是实现 source
接口。
pub trait Error: Debug + Display {
// 返回底层错误信息
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
// 返回错误的 TypeId 信息,使用默认即可
fn type_id(&self, _: private::Internal) -> TypeId
where
Self: 'static,
{
TypeId::of::<Self>()
}
// 可选,用于一些基于上下文的错误处理
fn provide<'a>(&'a self, request: &mut Request<'a>) {}
}
三方库
实践比较常用的是 thiserror 以及 anyhow 两个库,作者相同,分别用于库以及独立应用。其它还可以参考 snafu 这个库。
简单来说,通过 thiserror
可以通过简单的注解定义自己的 emnu
struct
类型的错误,而 anyhow
则针对错误提供更加便利的使用或者上下文等信息。如下同时记录 thiserror
常用的技巧:
// 直接从其它库转换
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
OtherError(#[from] OtherLibError)
}
其它
类型转换
对于 Option
可以通过 ok_or()
或者 ok_or_else()
转换为 Result
类型。
而 Result
转 Option
可以通过自定义的 ok()
函数实现,例如:
fn ok(self) -> Option<T> {
match self {
Ok(v) => Some(v),
Err(_) => None,
}
}
use std::{fs::File, io::ErrorKind};
fn main() {
match File::open("some_file_not_exists") {
Ok(f) => f,
Err(e) => match e.kind() {
ErrorKind::NotFound => {
println!("not found");
return;
}
_ => {
println!("unknown error {e}");
return;
}
},
};
}