文章482
标签257
分类63

Rust中的错误处理

本文讲解了如何Rust中进行错误处理;

源代码:


Rust中的错误处理

Result枚举

Rust 中没有提供类似于 Java、C++ 中的 Exception 机制,而是使用 Result 枚举的方式来实现:

pub enum Result<T, E> {
    /// Contains the success value
    Ok(T),
    /// Contains the error value
    Err(E),
}

在使用时:

  • 如果无错误则使用 Ok(T) 返回;
  • 如果存在错误,则使用 Err(E) 包装错误类型返回;

例如:

examples/0_result.rs

#[derive(Debug)]
pub enum MyError {
    Internal(String),
    InvalidId(String),
}

fn add(num: i64) -> Result<i64, MyError> {
    if num < 0 {
        Err(MyError::InvalidId(String::from("Invalid num!")))
    } else {
        Ok(num + 100000)
    }
}

fn main() -> Result<(), MyError> {
    // fetch_id(-1)?;

    let res = add(1)?;
    println!("{}", res);
    Ok(())
}

上面的代码首先通过 MyError 枚举定义了多个可能会出现的错误;

随后,在 add 函数中:

  • 当 num 小于 0 时返回错误;
  • 否则给 num 增加 100000 并返回;

在上面的 let res = add(1)?; 中使用了 ? 操作符,他相当于是一个语法糖:

  • 如果被调函数正常返回则调用 unwrap 获取其值;
  • 反之,则将被调函数的错误直接向上返回(相当于直接 return Err);

即上面的语法糖相当于:

let res = match add() {
  Ok(id) => id,
  Err(err) => {
    return Err(err);
  }
};

错误类型转换

上面简单展示了 Rust 中错误的使用;

由于 Rust 是强类型的语言,因此如果在一个函数中使用 ? 返回了多个错误,并且他们的类型是不同的,还需要对返回的错误类型进行转换,转为相同的类型!

例如下面的例子:

#[derive(Debug)]
pub enum MyError {
    ReadError(String),
    ParseError(String),
}

fn read_file() -> Result<i64, MyError> {
    // Error: Could not get compiled!
    let content = fs::read_to_string("/tmp/id")?;
    let id = content.parse::<i64>()?;
}

fn main() -> Result<(), MyError> {
    let id = read_file()?;
    println!("id: {}", id);
    Ok(())
}

上面的例子无法编译通过,原因在于: read_to_stringparse 返回的是不同类型的错误!

因此,如果要能返回,我们需要对每一个错误进行转换,转为我们所定义的 Error 类型;

例如:

examples/1_error_convert.rs

fn read_file() -> Result<i64, MyError> {
    // Error: Could not get compiled!
    // let content = fs::read_to_string("/tmp/id")?;
    // let id = content.parse::<i64>()?;

    // Method 1: Handling error explicitly!
    let content = match std::fs::read_to_string("/tmp/id") {
        Ok(content) => content,
        Err(err) => {
            return Err(MyError::ReadError(format!("read /tmp/id failed: {}", err)));
        }
    };
    let content = content.trim();
    println!("read content: {}", content);

    // Method 2: Use map_err to transform error type
    let id = content
        .parse::<i64>()
        .map_err(|err| MyError::ParseError(format!("parse error: {}", err)))?;

    Ok(id)
}

上面展示了两种不同的转换 Error 的方法:

方法一通过 match 匹配手动的对 read_to_string 函数的返回值进行处理,如果发生了 Error,则将错误转为我们指定类型的错误;

方法二通过 map_err 的方式,如果返回的是错误,则将其转为我们指定的类型,这时就可以使用 ? 返回了;

相比之下,使用 map_err 的方式,代码会清爽很多!


From Trait

上面处理错误的方法,每次都要对错误的类型进行转换,比较麻烦;

Rust 中提供了 From Trait,在进行类型匹配时,如果提供了从一个类型转换为另一个类型的方法(实现了某个类型的 From Trait),则在编译阶段,编译器会调用响应的函数,直接将其转为相应的类型!

例如:

examples/2_from_trait.rs

#[derive(Debug)]
pub enum MyError {
    ReadError(String),
    ParseError(String),
}

impl From<std::io::Error> for MyError {
    fn from(source: std::io::Error) -> Self {
        MyError::ReadError(source.to_string())
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(source: std::num::ParseIntError) -> Self {
        MyError::ParseError(source.to_string())
    }
}

fn read_file() -> Result<i64, MyError> {
    let _content = fs::read_to_string("/tmp/id")?;
    let content = _content.trim();
    let id = content.parse::<i64>()?;
    Ok(id)
}

fn main() -> Result<(), MyError> {
    let id = read_file()?;
    println!("id: {}", id);
    Ok(())
}

在上面的代码中,我们为 MyError 类型的错误分别实现了转换为 std::io::Errorstd::num::ParseIntError 类型的 From Trait;

因此,在 read_file 函数中就可以直接使用 ? 向上返回错误了!

但是上面的方法需要为每个错误实现 From Trait 还是有些麻烦,因此出现了 thiserror 以及 anyhow 库来解决这些问题;


其他第三方库

thiserror

上面提到了我们可以为每个错误实现 From Trait 来直接转换错误类型,thiserror 库就是使用这个逻辑;

我们可以使用 thiserror 库提供的宏来帮助我们生成到对应类型的 Trait;

例如:

examples/3_thiserror.rs

#[derive(thiserror::Error, Debug)]
pub enum MyError {
    #[error("io error.")]
    IoError(#[from] std::io::Error),
    #[error("parse error.")]
    ParseError(#[from] std::num::ParseIntError),
}

fn read_file() -> Result<i64, MyError> {
    // Could get compiled!
    let content = fs::read_to_string("/tmp/id")?;
    let id = content.parse::<i64>()?;
    Ok(id)
}

fn main() -> Result<(), MyError> {
    let id = read_file()?;
    println!("id: {}", id);
    Ok(())
}

我们只需要对我们定义的类型进行宏标注,在编译时这些宏会自动展开并实现对应的 Trait;

展开后的代码如下:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2018::*;
#[macro_use]
extern crate std;
use std::fs;
pub enum MyError {
    #[error("io error.")]
    IoError(#[from] std::io::Error),
    #[error("parse error.")]
    ParseError(#[from] std::num::ParseIntError),
}
#[allow(unused_qualifications)]
impl std::error::Error for MyError {
    fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
        use thiserror::__private::AsDynError;
        #[allow(deprecated)]
        match self {
            MyError::IoError { 0: source, .. } => std::option::Option::Some(source.as_dyn_error()),
            MyError::ParseError { 0: source, .. } => {
                std::option::Option::Some(source.as_dyn_error())
            }
        }
    }
}
#[allow(unused_qualifications)]
impl std::fmt::Display for MyError {
    fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        #[allow(unused_variables, deprecated, clippy::used_underscore_binding)]
        match self {
            MyError::IoError(_0) => {
                let result =
                    __formatter.write_fmt(::core::fmt::Arguments::new_v1(&["io error."], &[]));
                result
            }
            MyError::ParseError(_0) => {
                let result =
                    __formatter.write_fmt(::core::fmt::Arguments::new_v1(&["parse error."], &[]));
                result
            }
        }
    }
}
#[allow(unused_qualifications)]
impl std::convert::From<std::io::Error> for MyError {
    #[allow(deprecated)]
    fn from(source: std::io::Error) -> Self {
        MyError::IoError { 0: source }
    }
}
#[allow(unused_qualifications)]
impl std::convert::From<std::num::ParseIntError> for MyError {
    #[allow(deprecated)]
    fn from(source: std::num::ParseIntError) -> Self {
        MyError::ParseError { 0: source }
    }
}
#[automatically_derived]
#[allow(unused_qualifications)]
impl ::core::fmt::Debug for MyError {
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match (&*self,) {
            (&MyError::IoError(ref __self_0),) => {
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "IoError", &&*__self_0)
            }
            (&MyError::ParseError(ref __self_0),) => {
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ParseError", &&*__self_0)
            }
        }
    }
}
fn read_file() -> Result<i64, MyError> {
    let content = fs::read_to_string("/tmp/id")?;
    let id = content.parse::<i64>()?;
    Ok(id)
}
#[allow(dead_code)]
fn main() -> Result<(), MyError> {
    let id = read_file()?;
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(
            &["id: ", "\n"],
            &[::core::fmt::ArgumentV1::new_display(&id)],
        ));
    };
    Ok(())
}
#[rustc_main]
pub fn main() -> () {
    extern crate test;
    test::test_main_static(&[])
}

可以看到实际上就是为 MyError 实现了对应错误类型的 From Trait;

thiserror 库的这种实现方式,还需要为类型指定要转换的错误类型;

而下面看到的 anyhow 库,可以将错误类型统一为同一种形式;


anyhow

如果你对 Go 中的错误类型不陌生,那么你就可以直接上手 anyhow 了!

来看下面的例子:

examples/4_anyhow.rs

use anyhow::Result;
use std::fs;

fn read_file() -> Result<i64> {
    // Could get compiled!
    let content = fs::read_to_string("/tmp/id")?;
    let id = content.parse::<i64>()?;
    Ok(id)
}

fn main() -> Result<()> {
    let id = read_file()?;
    println!("id: {}", id);
    Ok(())
}

注意到,上面的 Result 类型为 anyhow::Result,而非标准库中的 Result 类型!

anyhowResult<T, E> 实现了 Context Trait:

impl<T, E> Context<T, E> for Result<T, E> where
    E: ext::StdError + Send + Sync + 'static,
{
    fn context<C>(self, context: C) -> Result<T, Error>
    where
        C: Display + Send + Sync + 'static,
    {
        // Not using map_err to save 2 useless frames off the captured backtrace
        // in ext_context.
        match self {
            Ok(ok) => Ok(ok),
            Err(error) => Err(error.ext_context(context)),
        }
    }

    fn with_context<C, F>(self, context: F) -> Result<T, Error>
    where
        C: Display + Send + Sync + 'static,
        F: FnOnce() -> C,
    {
        match self {
            Ok(ok) => Ok(ok),
            Err(error) => Err(error.ext_context(context())),
        }
    }
}

Context 中提供了 context 函数,并且将原来的 Result<T, E> 转成了 Result<T, anyhow::Error>

因此,最终将错误类型统一为了 anyhow::Error 类型;


附录

源代码:

文档:



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2022/11/18/Rust中的错误处理/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可