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 VS. expect
如下代码会抛出异常,默认没有打印调用栈,如果要打印,需要添加 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 退出,但是在示例代码中会大大简化处理逻辑。
通过 expect() 可以传递一些上下文信息,而 unwrap() 只会报错,两者效果相同,可以在使用时选择。
另外,还可以通过 ? 问号来简化 Option 和 Result 的处理,如果有值会将值提取出来,否则返回,此时函数也要求返回 Option 或者 Result 类型。
自定义
很多的库中需要自定义错误,大部分场景需要调用底层的接口,而底层可能对应了不同的错误类型,所以需要同时处理不同的类型的错误转换,例如如下的示例。
#[derive(Debug)]
pub enum Error {
Internal(String),
// 某个错误里已经包含了详细的错误信息以及上下文
Parse { source: url::ParseError },
}
// 应用标准接口,有部分默认实现
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::Internal(msg) => write!(f, "internal error: {msg}"),
Error::Parse { source } => write!(f, "parse url error: {source}"),
}
}
}
// 处理通过From作隐式转换,还可以通过 map_err 实现
impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Self {
Error::Parse { source: e }
}
}
// 覆盖原有的Result类型,提供默认的Error信息,也可以在某些地方指定具体错误
type Result<T, E = Error> = std::result::Result<T, E>;
fn save(input: &str) -> Result<()> {
let url = url::Url::parse(input)?;
println!("schema is {}", url.scheme());
Ok(())
}
// 还可以使用宏创建报错
#[macro_export]
macro_rules! errinternal {
// ($($args:tt)*) => { $crate::Error::Internal(format!($($args)*)) };
($($args:tt)*) => { $crate::Error::Internal(format!($($args)*)).into() };
}
// 配合上述的宏使用,可以简化后续的使用,否则就需要 return Err(errinternal(xxx)); 方式
impl<T> From<Error> for Result<T> {
fn from(error: Error) -> Self {
Err(error)
}
}
fn foobar() -> Result<()> {
return errinternal!("hello");
}
fn main() {
println!("{:?}", foobar());
save("http://example.org").unwrap();
// 将错误转换为String类型
println!("{:?}", save("error").map_err(|e| e.to_string()));
println!(
"{:?}",
save("error").map_err(|e| Error::Internal(format!("parse url failed: {e}")))
);
println!("{:?}", save("error").map_err(Error::from)); // 自动调用上述的转换
}
通过 map_err 可以将 Error 类型进行转换。
标准库
标准库提供了 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)
}
其它
AS
在 Option<T> 中包含了 as_ref() as_deref() as_deref_mut() 几个函数,会对变量的 T 类型进行转换,如下仅介绍 as_ref() 的使用,其它两个就是解引用的使用。
将 Option<T> 转换为 Option<&T> 类型,也就是引用内部的 T 类型,其在标准库中的实现也很简单。
pub const fn as_ref(&self) -> Option<&T> {
match *self {
Some(ref x) => Some(x),
None => None,
}
}
用来获取 T 的引用,这样不会发生所有权的转移。
fn main() {
let data = Some(String::from("Hello"));
//let size = data.map(|s| s.len()); error: used after remove
let size = data.as_ref().map(|s| s.len());
println!("'{}' length is {}", data.unwrap(), size.unwrap());
}
如果不使用 as_ref() 就会报 used after remove 的错误。
类型转换
对于 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;
}
},
};
}