初学 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实现Fromtrait 才行。
- 使用 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
}
参考:
推荐:GreptimeDB 如何使用 snafu 处理复杂项目的错误类型
Rust error handling is perfect actually
rust by example - 处理多种错误类型
Option 和 Result 的一些方法
rust doc Error Handling
简谈 Rust 中的错误处理
细说 rust 错误处理
?在 Result 中的使用
推荐:蚂蚁集团 CeresDB 团队 | 关于 Rust 错误处理的思考