[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),
}

在使用時:

例如:

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 函數中:

在上面的 let res = add(1)?; 中使用了 ? 操作符,他相當於是一個語法糖:

即上面的語法糖相當於:

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_string 和 parse 返回的是不同類型的錯誤!

因此,如果要能返回,我們需要對每一個錯誤進行轉換,轉爲我們所定義的 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::Error 和 std::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 類型!

anyhow 爲 Result<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 類型;

附錄

源代碼:

文檔:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_GoTeoYVlimhjQ6Repw6Iw