Rust 中的错误处理
本文的主要内容是简要介绍在 Rust 错误处理相关的内容,以及如何使用 thiserror 和 anyhow 简化错误处理流程。
2024-04-18
在 Rust 中有两类错误:
- 不可恢复错误:指那些影响程序运行、不可恢复的错误,比如数组越界。对于这种类型的错误,可以使用 宏直接终止程序。
panic!
- 可恢复错误:指那些不影响应用运行、可以被用户解决的错误,比如“文件不存在”错误。对于这种类型的错误,可以通过 类型传递错误并交由程序中对应的模块进行处理;
Result<T, E>
错误传播(Propagating Errors)机制
Rust 中的错误传播(Propagating Errors)指的是在错误发生后不在函数内部进行处理,而是将错误直接返回给函数调用方,比如在下面的代码中,在处理
username_file_result
panic!
Err
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),
}
}
使用 Result<T, E>
处理可恢复错误
Result<T, E>
在 Rust 中可以
Result<T, E>
Result<T, E>
match
if let
// `T` 为执行成功的返回的结果的类型,`E` 为错误的类型
enum Result<T, E> {
Ok(T),
Err(E),
}
使用
Result<T, E>
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
除了直接使用分支匹配进行处理外,还可以使用下面的方法在处理前先对结果进行转换:
- 方法:使用闭包
map(fn)
对fn
值进行映射;Ok
- 方法:使用闭包
map_err(fn)
对fn
值进行映射;Err
- 方法:如果结果为
map_or(default, fn)
则返回Err
值,否则使用闭包default
对fn
值进行映射;Ok
- 方法:如果结果为
map_or_else(default, fn)
则使用闭包Err
获取一个默认值,否则使用闭包default
对fn
值进行映射;Ok
- :如果结果为
and(res)
则返回Ok
(也是一个res
类型的实例),否则返回Result
;Err
- 方法:如果结果为
and_then(op)
则调用Ok
方法并返回另一个op
,否则返回Result
;Err
unwrap
、expect
和 ?
的区别
unwrap
expect
?
当一个方法返回
Result<T, E>
unwrap
expect
?
- 和
unwrap
在遇到expect
分支时会触发 panic 终止程序,而Err
操作符会自动进行错误传播,将?
返回给方法调用者。Err
- 除了终止程序外,还能够手动添加更多的信息,因此在生产环境中更常使用
expect
而非expect
。unwrap
在 Rust 中自定义错误类型
使用
Err
- trait:用于错误信息的展示;
fmt::Display
- trait:让自定义错误类型和 Rust 标准错误类型兼容。
std::error::Error
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct MyError {
message: String,
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Some error occurred: {}", self.message)
}
}
impl Error for MyError {
fn description(&self) -> &str {
&self.message
}
}
fn bad_function() -> Result<(), MyError> {
Err(MyError {
message: "error".to_string(),
})
}
fn main() {
match bad_function() {
Ok(_) => (),
Err(e) => println!("{}", e.to_string())
}
}
src/main.rs
上面这段代码遵循了标准做法实现了
MyError
- 实现 trait 中的
std::error::Error
方法可以实现错误链接(Error Chaining),便于调试;source()
- 实现 trait 能够和系统库、第三方库保持兼容;
std::error::Error
- 实现 trait 能够提供更加表义的错误信息。
fmt::Display
比如在这个例子中,如果自定义错误类型
EmptyVec
std::error::Error
double_first
Error
Box<dyn Error>
Err
在实际项目中,手动实现所有错误类型的过程很繁琐,在 Rust 生态中更常使用 库来简化这一过程。thiserror
使用 thiserror
简化错误类型定义
thiserror
thiserror
- :为错误类型提供描述信息字符串模版,在模版中可以使用
#[error("...")]
、{0}
等占位符插入变体的数据。{1}
- :自动实现
#[from] std::io::Error
trait,比如在上面的代码,From
能够自动转换为std::io::Error
。MyError::IoError
- :直接转发底层错误的源和
#[error(transparent)]
方法,常用于定义“其他”错误。Display
使用
thiserror
// cargo add thiserror
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("Some error occurred: {0}")]
SomeError(String),
}
fn bad_function() -> Result<(), MyError> {
Err(MyError::SomeError("error".to_string()))
}
fn main() {
match bad_function() {
Ok(_) => (),
Err(e) => println!("{}", e.to_string())
}
}
error.rs
使用 anyhow 简化错误传播过程
如前所述,错误传播指的是将错误返回给调用方,由调用方对错误进行处理。在处理错误传播的过程中,虽然能够使用
?
Result<T, E>
E
fn read_age() -> Result<u32, String> {
let mut age = String::new();
println!("Input Your Age: ");
std::io::stdin()
.read_line(&mut age)
.map_err(|_| "Failed to read age")?;
Ok(age
.trim()
.parse()
.map_err(|_: std::num::ParseIntError| "Failed to parse age")?)
}
fn check_age(age: u32, min: u32, max: u32) -> Result<(), String> {
if age < min {
return Err("Too young".to_string());
}
if age > max {
return Err("Too old".to_string());
}
Ok(())
}
fn get_age_between(min: u32, max: u32) -> Result<u32, String> {
let age = read_age()?;
check_age(min, max)?;
Ok(age)
}
fn main() {
match get_age_between(18, 60) {
Ok(a) => println!("Age: {}", a),
Err(e) => println!("Error: {}", e.to_string()),
};
}
main.rs
为了将
Err
map_err
String
anyhow
在
anyhow
anyhow!
with_context
- 宏能够捕获错误的来源,并保留内部错误的上下文,使用上下文信息及用户指定的参数构造一个
anyhow!()
实例;anyhow::Error
- 方法可以为错误构造上下文,提供更多的信息。
with_context
使用
anyhow
// cargo add anyhow
use anyhow::{anyhow, Result};
fn read_age() -> Result<u32> {
let mut age = String::new();
println!("Input Your Age: ");
std::io::stdin()
.read_line(&mut age)
.with_context(|| "Failed to read age {}")?;
Ok(age.trim().parse().with_context(|| "Failed to parse age")?)
}
fn check_age(age: u32, min: u32, max: u32) -> Result<()> {
if age < min {
return Err(anyhow!("Too young".to_string()));
}
if age > max {
return Err(anyhow!("Too old".to_string()));
}
Ok(())
}
fn get_age_between(min: u32, max: u32) -> Result<u32> {
let age = read_age()?;
check_age(age, min, max)?;
Ok(age)
}
main.rs
重写后的主要区别在于:
- 使用 替代默认
anyhow::Result<T>
,不再需要指定错误类型,所有的错误类型统一为Result<T, E>
,传播过程中的转换由anyhow::Error
完成;anyhow
- 对于标准库方法(、
std::io::stdin
)使用parse
提供上下文并生成with_context
;anyhow::Error
- 使用 宏生产
anyhow::anyhow!
。anyhow::Error
需要注意的是,
anyhow::Error
结合 thiserror 和 anyhow 进行错误处理
上述例子直接使用
anyhow
下面这段代码中结合
thiserror
anyhow
use anyhow::Result;
use thiserror::Error;
#[derive(Error, Debug)]
enum AgeError {
#[error("Failed to parse: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("Too young")]
TooYoung,
#[error("Too old")]
TooOld,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
fn read_age() -> Result<u32> {
let mut age = String::new();
println!("Input Your Age: ");
std::io::stdin()
.read_line(&mut age)
.map_err(|e| AgeError::Other(e.into()))?;
Ok(age.trim().parse().map_err(AgeError::ParseError)?)
}
fn check_age(age: u32, min: u32, max: u32) -> Result<()> {
if age < min {
return Err(AgeError::TooYoung.into());
}
if age > max {
return Err(AgeError::TooOld.into());
}
Ok(())
}
fn get_age_between(min: u32, max: u32) -> Result<u32> {
let age = read_age()?;
check_age(age, min, max)?;
Ok(age)
}
fn main() {
match get_age_between(18, 60) {
Ok(a) => println!("Age: {}", a),
Err(e) => print!("Error: {}", e.to_string()),
};
}
main.rs