在 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>
处理可恢复错误
在 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
和 ?
的区别
当一个方法返回 Result<T, E>
枚举时,除了使用分支匹配(match)处理外,还可以使用 unwrap
、expect
和 ?
来理简化处理流程,这三者的区别在与:
unwrap
和expect
在遇到Err
分支时会触发 panic 终止程序,而?
操作符会自动进行错误传播,将Err
返回给方法调用者。expect
除了终止程序外,还能够手动添加更多的信息,因此在生产环境中更常使用expect
而非unwrap
。
在 Rust 中自定义错误类型
使用 Err
返回错误实例时,可以使用任意类型的值,因此在项目中自定义错误类型的方法多种多样(比如定义 struct 或定义 Enum),但一般而言,在 Rust 中自定义错误类型时都需要实现两个 trait:
fmt::Display
trait:用于错误信息的展示;std::error::Error
trait:让自定义错误类型和 Rust 标准错误类型兼容。
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
错误,这样做的好处是:
- 实现
std::error::Error
trait 中的source()
方法可以实现错误链接(Error Chaining),便于调试; - 实现
std::error::Error
trait 能够和系统库、第三方库保持兼容; - 实现
fmt::Display
trait 能够提供更加表义的错误信息。
比如在这个例子中,如果自定义错误类型 EmptyVec
没有实现 std::error::Error
trait,并且 double_first
方法中出现其他类型的 Error
,则需要手动转换错误类型,而不能直接使用 Box<dyn Error>
来标识 Err
类型。
在实际项目中,手动实现所有错误类型的过程很繁琐,在 Rust 生态中更常使用 thiserror
库来简化这一过程。
使用 thiserror
简化错误类型定义
thiserror
提供了三个宏用于简化错误类型的定义过程,这三个宏及其作用为:
#[error("...")]
:为错误类型提供描述信息字符串模版,在模版中可以使用{0}
、{1}
等占位符插入变体的数据。#[from] std::io::Error
:自动实现From
trait,比如在上面的代码,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
返回,并不包含相关的 BACKTRACE 信息)。针对这种情况,可以使用 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