初学rust的时候,上手写代码总是遇到很多不一样的rust的Result
类型,不同crate中的函数返回的Result<T, E>
的E
都不一样,刚开始都是unwrap
或者expect
来处理。如果使用try!
或者?
的话总是编译不通过,还是对Error转换和处理不熟练。
Error转换
在一个方法中,调用不同的函数会返回不同的error类型,需要你将这些类型转换成统一的自定义error类型再返回。你有以下几种途径
使用map_err
fn cook_pasta() -> Result<Pasta, CookingError> {
let water = boil_water().map_err(|_| CookingError::BoilWaterError)?;
let pasta = add_pasta(&water).map_err(|_| CookingError::AddPastaError)?;
Ok(pasta)
}
// 通过map_err将boil_water()和add_pasta(&water)返回的error都转换成了CookingError类型
使用std::error::Error+From trait
定义自己的Error类型并实现From trait。 From trait用于将boil_water()和add_pasta(&water)的error转换成自定义的Error。其实就是将map_err
的逻辑移动到From trait中实现,使得方法调用处看起来更简洁。
pub enum CookingError{
BoilWaterError(String),
AddPastaError
}
impl std::error::Error for CookingError{
// ...
}
impl Display for CookingError{
// ...
}
// 假设boil_water返回的error是NoWaterError
impl From<NoWaterError> for CookingError {
fn from(s: NoWaterError) -> Self {
CookingError::BoilWaterError(s)
}
}
// 假设add_pasta返回的error是IoError
impl From<IoError> for CustomError {
fn from(s: std::io::Error) -> Self {
CookingError::AddPastaError(s)
}
}
// 无需map_err
fn cook_pasta() -> Result<Pasta, CookingError> {
let water = boil_water()?; // 如果抛出NoWaterError,自动转成CookingError::BoilWaterError,下面同理
let pasta = add_pasta(&water)?;
Ok(pasta)
}
thiserror
thiserror 可以看作是定义 Error 的一个工具,它只帮你生成一些定义 Error 的代码,别的什么都不做,相当纯粹。如果你在开发一个crate,那么建议使用thiserror。
fn render() -> Result<String, std::io::Error> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(source)
}
上面的代码无法通过编译,因为env::var()
返回的是 std::env::VarError
,而 read_to_string()
返回的是 std::io::Error
。
为了满足 render 函数的签名,我们就需要将 env::VarError 和 io::Error 归一化为同一种错误类型。要实现这个目的有三种方式:
- 使用特征对象
Box<dyn Error>
。 实际上就是错误类型泛化,失去了具体错误类型的信息,类似于在Java中使用Object
类型。 - 自定义错误类型。 比较繁琐,上面的例子在自定义Error类型后,需要分别为
env::VarError
和io::Error
实现From
trait才行。 - 使用
thiserror
。 简化自定义错误类型的繁琐:
use std::fs::read_to_string;
fn main() -> Result<(), MyError> {
let html = render()?;
println!("{}", html);
Ok(())
}
fn render() -> Result<String, MyError> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file)?;
Ok(source)
}
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("Environment variable not found")]
EnvironmentVariableNotFound(#[from] std::env::VarError),
#[error(transparent)]
IOError(#[from] std::io::Error),
}
thiserror
提供#[from]
#[error]
等注解简化错误类型自定义工作。
#[derive(Error)]
{
// Attributes available to this derive:
#[backtrace] //
#[error]
#[from]
#[source]
}
error(transparent)
表示转发底层error的相关信息,不修改source和Display相关方法。
anyhow
anyhow 为你定义好了一个 Error 类型,基本可以看作是一个 Box
use anyhow::Result;
fn main() -> Result<()> {
let html = render()?;
println!("{}", html);
Ok(())
}
fn render() -> Result<String> {
let file = std::env::var("MARKDOWN")?;
let source = read_to_string(file).with_context(|| format!("read string from {} failed", &file))?
Ok(source)
}
可以看到这里使用的是Result<String>
,实际上这是anyhow
的type alias:pub type Result<T, E = Error> = core::result::Result<T, E>;
anyhow
还提供了with_context
给error添加信息,看上去和expect类似,只不过那是panic。
对于一个app服务,一些核心登录等功能可能也需要自定义error类型,这时可以将anyhow::Error
作为其中一种error类型,即thiserror + anyhow:
#[derive(Error, Debug)]
pub enum AppError {
...
#[error(transparent)]
Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error
}
参考:
rust by example - 处理多种错误类型
Option和Result的一些方法
rust doc Error Handling
简谈 Rust 中的错误处理
细说rust错误处理
?在Result中的使用
蚂蚁集团 CeresDB 团队 | 关于 Rust 错误处理的思考