Rust 錯誤處理

rust 處理錯誤,不使用 try catch, 而是使用 Result<T, E>。

簡單的處理rust錯誤

在各種關於rust錯誤處理的文檔中,爲了解釋清楚其背後的機制,看着內容很多,不好理解。

比如我們寫一個方法,讀取文件內容:

fn read_file_to_string(file_path: String) -> Result<String, io::Error>{
    let mut file = File::open(file_path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

上面的代碼,當文件不存在的時候,也可以很好的返回異常信息。

調用代碼:

    let r = read_file_to_string(r"d:\1111.txt".to_string());
    match r {
        Ok(str) => println!("OK: {str}"),
        Err(e) => println!("Error: {e}"),
    };

如果文件不存在,會輸出信息:

這個異常處理的過程不復雜,分爲三步:

  1. 自定義的函數要返回Result<T,E>,

  2. 返回Result的函數時,後面加上問號,

  3. 在最上層,使用match處理結果。

但是這樣是不夠的,如果在一個大項目中,我們很難找到是哪個文件缺失了,rust不像c#那樣,可以很容易的獲取到出現問題的代碼行數、類和方法名等。

最直觀的方法是,在異常信息裏,帶上文件名。

自定義錯誤,帶上文件名

rust自定義錯誤分爲三步:

1)定義錯誤類型

2)實現Error特徵(trait)

3)  實現Display特徵

自定義錯誤的類型是enum, 和其他語言相比,這有點奇怪。 代碼如下:

// 定義自定義錯誤類型
#[derive(Debug)]
pub enum MyError {
    FileOpenError(String),
    ParseError(String),
    Common(String),
}

// 實現Error特質
impl Error for MyError {}

// 實現Display特質以便打印錯誤信息
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MyError::FileOpenError(msg) => write!(f, "Failed to open file: {}", msg),
            MyError::ParseError(msg) => write!(f, "Parse error: {}", msg),
            MyError::Common(msg) => write!(f, "Other error: {}", msg),
        }
    }
}

這時,讀取文件的函數代碼要改成這樣:

fn read_file_to_string(file_path: String) -> Result<String, MyError>{
    let r = File::open(file_path.clone());
    match r {
        Ok(mut file) => {
            let mut contents = String::new();
            let r2 = file.read_to_string(&mut contents);
            match r2 {
                Ok(size) => return Ok( contents),
                Err(e) => return Err(MyError::Common(format!("{e} 文件: {file_path}"))),
            } 
        },
        Err(e) => {
            return Err(MyError::FileOpenError(format!("{e} 文件: {file_path}")));
        },
    }
}

代碼變得很囉嗦,好在能比較好的顯示錯誤了:

自定義錯誤的三部曲,雖然有點長,但是這是項目的公共代碼,還是可以忍受的。讀取文件的代碼,和 c#比起來,真的太羅嗦了。

簡化通用異常處理

讀取文件內容的函數,代碼羅嗦的原因是,異常類型通過問號匹配到自定義的MyError很麻煩。

這裏我們採用一種更通用的方式,來處理異常:

1) 重新定義異常類型,並且提供其他異常向自定義異常轉換的方法

custom_error.rs:

use std::error::Error;
use std::fmt;
use std::fmt::Display;

// 自定義錯誤類型,包含文件路徑信息
#[derive(Debug)]
pub struct MyError {
    msg: String,
    source: String,
}

// 爲自定義錯誤類型實現Error trait
impl Error for MyError {}

// 實現Display trait,以便於打印錯誤信息
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}: {}", self.source, self.msg)
    }
}

pub fn convert_error(msg:String, err: String) -> MyError {
    MyError {
        msg: msg ,
        source: err.to_string(),
    }
}

// 定義一個新的trait
pub trait MyErrorExtension<T> {
    fn ex_err(self, msg:&String)->  Result<T, MyError>;
}

// 爲Result<T,E>類型實現MyExtension trait
impl<T,E:Display> MyErrorExtension<T> for Result<T,E> {
    fn ex_err(self, msg:&String) ->  Result<T, MyError> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => Err(MyError{msg:msg.to_string(), source: e.to_string()}),
        }
    }
}

2) 定義帶有通用異常處理能力的函數的示例:

fn read_file_to_string(file_path: String) -> Result<String, MyError>{
    let context_info = format!("文件路徑: {file_path}");
    fs::metadata(&file_path).ex_err(&context_info)?;
    let mut file = File::open(&file_path).ex_err( &context_info)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).ex_err(&context_info)?;
    Ok(contents)
}

以打開文件的方法爲例,原本的調用是:

let mut file = File::open(&file_path)?;

新的調用,後面附加了重要的上下文信息,並且把異常類型轉換爲MyError:

let mut file = File::open(&file_path).ex_err( &context_info)?;

通過擴展方法ex_err, 達到了我們的目的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章