細說Rust錯誤處理

image.png

原文地址:https://github.com/baoyachi/rust-error-handle

1. 前言

這篇文章寫得比較長,全文讀完大約需要15-20min,如果對Rust的錯誤處理不清楚或還有些許模糊的同學,請靜下心來細細閱讀。當讀完該篇文章后,可以說對Rust的錯誤處理可以做到掌握自如。

筆者花費較長篇幅來描述錯誤處理的來去,詳細介紹其及一步步梳理內容,望大家能耐心讀完后對大家有所幫助。當然,在寫這篇文章之時,也借閱了大量互聯網資料,詳見鏈接見底部參考鏈接

掌握好Rust的錯誤設計,不僅可以提升我們對錯誤處理的認識,對代碼結構、層次都有很大的幫助。那廢話不多說,那我們開啟這段閱讀之旅吧??!

2. 背景

筆者在寫這篇文章時,也翻閱一些資料關于Rust的錯誤處理資料,多數是對其一筆帶過,導致之前接觸過其他語言的新同學來說,上手處理Rust的錯誤會有當頭棒喝的感覺。找些資料發現unwrap()也可以解決問題,然后心中暗自竊喜,程序在運行過程中,因為忽略檢查或程序邏輯判斷,導致某些情況,程序panic。這可能是我們最不愿看到的現象,遂又回到起點,重新去了解Rust的錯誤處理。

這篇文章,通過一步步介紹,讓大家清晰知道Rust的錯誤處理的究竟。介紹在Rust中的錯誤使用及如何處理錯誤,以及在實際工作中關于其使用技巧。

3. unwrap的危害!

下面我們來看一段代碼,執行一下:

fn main() {
    let path = "/tmp/dat";
    println!("{}", read_file(path));
}

fn read_file(path: &str) -> String {
    std::fs::read_to_string(path).unwrap()
}

程序執行結果:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1188:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
  ...
  15: rust_sugar::read_file
             at src/main.rs:7
  16: rust_sugar::main
             at src/main.rs:3
  ...
  25: rust_sugar::read_file
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

什么,因為path路徑不對,程序竟然崩潰了,這個是我們不能接受的!

unwrap() 這個操作在rust代碼中,應該看過很多這種代碼,甚至此時我們正在使用它。它主要用于OptionResult的打開其包裝的結果。常常我們在代碼中,使用簡單,或快速處理,使用了 unwrap() 的操作,但是,它是一個非常危險的信號!

可能因為沒有程序檢查或校驗,潛在的bug可能就出現其中,使得我們程序往往就panic了。這可能使我們最不愿看到的現象。

在實際項目開發中,程序中可能充斥著大量代碼,我們很難避免unwrap()的出現,為了解決這種問題,我們通過做code review,或使用腳本工具檢查其降低其出現的可能性。

通常每個項目都有一些約束,或許:在大型項目開發中, 不用unwrap() 方法,使用其他方式處理程序,unwrap() 的不出現可能會使得程序的健壯性高出很多。

這里前提是團隊或大型項目,如果只是寫一個簡單例子(demo)就不在本篇文章的討論范疇。因為一個Demo的問題,可能只是快速示范或演示,不考慮程序健壯性, unwrap() 的操作可能會更方便代碼表達。

可能有人會問,我們通常跑程序unit test,其中的很多mock數據會有 unwrap() 的操作,我們只是為了在單元測試中使得程序簡單。這種也能不使用嗎?答案:是的,完全可以不使用 unwrap() 也可以做到的。

4. 對比語言處理錯誤

說到unwrap(),我們不得不提到rust的錯誤處理,unwrap()Rust的錯誤處理是密不可分的。

4.1 golang的錯誤處理演示

如果了解golang的話,應該清楚下面這段代碼的意思:

package main

import (
    "io/ioutil"
    "log"
)

func main() {
    path := "/tmp/dat"  //文件路徑
    file, err := readFile(path) 
    if err != nil {
        log.Fatal(err) //錯誤打印
    }
    println("%s", file) //打印文件內容
}

func readFile(path string) (string, error) {
    dat, err := ioutil.ReadFile(path)  //讀取文件內容
    if err != nil {  //判斷err是否為nil
        return "", err  //不為nil,返回err結果
    }
    return string(dat), nil  //err=nil,返回讀取文件內容
}

我們執行下程序,打印如下。執行錯誤,當然,因為我們給的文件路徑不存在,程序報錯。

2020/02/24 01:24:04 open /tmp/dat: no such file or directory

這里,golang采用多返回值方式,程序報錯返回錯誤問題,通過判斷 err!=nil 來決定程序是否繼續執行或終止該邏輯。當然,如果接觸過golang項目時,會發現程序中大量充斥著if err!=nil的代碼,對此網上有對if err!=nil進行了很多討論,因為這個不在本篇文章的范疇中,在此不對其追溯、討論。

4.2 Rust 錯誤處理示例

對比了golang代碼,我們對照上面的例子,看下在Rust中如何編寫這段程序,代碼如下:

fn main() {
    let path = "/tmp/dat";  //文件路徑
    match read_file(path) { //判斷方法結果
        Ok(file) => { println!("{}", file) } //OK 代表讀取到文件內容,正確打印文件內容
        Err(e) => { println!("{} {}", path, e) } //Err代表結果不存在,打印錯誤結果
    }
}

fn read_file(path: &str) -> Result<String,std::io::Error> { //Result作為結果返回值
    std::fs::read_to_string(path) //讀取文件內容
}

當前,因為我們給的文件路徑不存在,程序報錯,打印內容如下:

No such file or directory (os error 2)

Rust代表中,Result是一個enum枚舉對象,部分源碼如下:

pub enum Result<T, E> {
    /// Contains the success value
    Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

    /// Contains the error value
    Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}

通常我們使用Result的枚舉對象作為程序的返回值,通過Result來判斷其結果,我們使用match匹配的方式來獲取Result的內容,判斷正常(Ok)或錯誤(Err)。

或許,我們大致向上看去,golang代碼和Rust代碼沒有本質區別,都是采用返回值方式,給出程序結果。下面我們就對比兩種語言說說之間區別:

  • golang采用多返回值方式,我們在拿到目標結果時(上面是指文件內容file),需要首先對err判斷是否為nil,并且我們在return時,需要給多返回值分別賦值,調用時需要對 if err!=nil 做結果判斷。
  • Rust中采用Result的枚舉對象做結果返回。枚舉的好處是:多選一。因為Result的枚舉類型為OkErr,使得我們每次在返回Result的結果時,要么是Ok,要么是Err。它不需要return結果同時給兩個值賦值,這樣的情況只會存在一種可能性: Ok or Err 。
  • golang的函數調用需要對 if err!=nil做結果判斷,因為這段代碼 判斷是手動邏輯,往往我們可能因為疏忽,導致這段邏輯缺失,缺少校驗。當然,我們在編寫代碼期間可以通過某些工具 lint 掃描出這種潛在bug。
  • Rustmatch判斷是自動打開,當然你也可以選擇忽略其中某一個枚舉值,我們不在此說明。

可能有人發現,如果我有多個函數,需要多個函數的執行結果,這樣需要match代碼多次,代碼會不會是一坨一坨,顯得代碼很臃腫,難看。是的,這個問題提出的的確是有這種問題,不過這個在后面我們講解的時候,會通過程序語法糖避免多次match多次結果的問題,不過我們在此先不敘說,后面將有介紹。

5. Rust中的錯誤處理

前面不管是golang還是Rust采用return返回值方式,兩者都是為了解決程序中錯誤處理的問題。好了,前面說了這么多,我們還是回歸正題:Rust中是如何對錯誤進行處理的?

要想細致了解Rust的錯誤處理,我們需要了解std::error::Error,該trait的內部方法,部分代碼如下:
參考鏈接:https://doc.rust-lang.org/std/error/trait.Error.html

pub trait Error: Debug + Display {

    fn description(&self) -> &str {
        "description() is deprecated; use Display"
    }

    #[rustc_deprecated(since = "1.33.0", reason = "replaced by Error::source, which can support \
                                                   downcasting")]

    fn cause(&self) -> Option<&dyn Error> {
        self.source()
    }

    fn source(&self) -> Option<&(dyn Error + 'static)> { None }

    #[doc(hidden)]
    fn type_id(&self, _: private::Internal) -> TypeId where Self: 'static {
        TypeId::of::<Self>()
    }

    #[unstable(feature = "backtrace", issue = "53487")]
    fn backtrace(&self) -> Option<&Backtrace> {
        None
    }
}
  • description()在文檔介紹中,盡管使用它不會導致編譯警告,但新代碼應該實現impl Display ,新impl的可以省略,不用實現該方法, 要獲取字符串形式的錯誤描述,請使用to_string()

  • cause()1.33.0被拋棄,取而代之使用source()方法,新impl的不用實現該方法。

  • source()此錯誤的低級源,如果內部有錯誤類型Err返回:Some(e),如果沒有返回:None。

    • 如果當前Error是低級別的Error,并沒有子Error,需要返回None。介于其本身默認有返回值None,可以不覆蓋該方法。
    • 如果當前Error包含子Error,需要返回子ErrorSome(err),需要覆蓋該方法。
  • type_id()該方法被隱藏。

  • backtrace()返回發生此錯誤的堆棧追溯,因為標記unstable,在Ruststable版本不被使用。

  • 自定義的Error需要impl std::fmt::Debug的trait,當然我們只需要在默認對象上添加注解:#[derive(Debug)]即可。

總結一下,自定義一個error需要實現如下幾步:

  • 手動實現impl std::fmt::Display的trait,并實現 fmt(...)方法。
  • 手動實現impl std::fmt::Debugtrait,一般直接添加注解即可:#[derive(Debug)]
  • 手動實現impl std::error::Errortrait,并根據自身error級別是否覆蓋std::error::Error中的source()方法。

下面我們自己手動實現下Rust自定義錯誤:CustomError

use std::error::Error;

///自定義類型 Error,實現std::fmt::Debug的trait
#[derive(Debug)]
struct CustomError {
    err: ChildError,
}

///實現Display的trait,并實現fmt方法
impl std::fmt::Display for CustomError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "CustomError is here!")
    }
}

///實現Error的trait,因為有子Error:ChildError,需要覆蓋source()方法,返回Some(err)
impl std::error::Error for CustomError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.err)
    }
}


///子類型 Error,實現std::fmt::Debug的trait
#[derive(Debug)]
struct ChildError;

///實現Display的trait,并實現fmt方法
impl std::fmt::Display for ChildError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ChildError is here!")
    }
}

///實現Error的trait,因為沒有子Error,不需要覆蓋source()方法
impl std::error::Error for ChildError {}

///構建一個Result的結果,返回自定義的error:CustomError
fn get_super_error() -> Result<(), CustomError> {
    Err(CustomError { err: ChildError })
}

fn main() {
    match get_super_error() {
        Err(e) => {
            println!("Error: {}", e);
            println!("Caused by: {}", e.source().unwrap());
        }
        _ => println!("No error"),
    }
}
  • ChildError為子類型Error,沒有覆蓋source()方法,空實現了std::error::Error
  • CustomError有子類型ChildError,覆蓋source(),并返回了子類型Option值:Some(&self.err)

運行執行結果,顯示如下:

Error: CustomError is here!
Caused by: ChildError is here!

至此,我們就了解了如何實現Rust自定義Error了。

6. 自定義Error轉換:From

上面我們說到,函數返回Result的結果時,需要獲取函數的返回值是成功(Ok)還是失敗(Err),需要使用match匹配,我們看下多函數之間調用是如何解決這類問題的?假設我們有個場景:

  • 讀取一文件
  • 將文件內容轉化為UTF8格式
  • 將轉換后格式內容轉為u32的數字。

所以我們有了下面三個函數(省略部分代碼):

...

///讀取文件內容
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 轉化為u32數字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}

最終,我們得到u32的數字,對于該場景如何組織我們代碼呢?

  • unwrap()直接打開三個方法,取出值。這種方式太暴力,并且會有bug,造成程序panic,不被采納。
  • match匹配,如何返回OK,繼續下一步,否則報錯終止邏輯,那我們試試。

參考代碼如下:

fn main() {
    let path = "./dat";
    match read_file(path) {
        Ok(v) => {
            match to_utf8(v.as_bytes()) {
                Ok(u) => {
                    match to_u32(u) {
                        Ok(t) => {
                            println!("num:{:?}", u);
                        }
                        Err(e) => {
                            println!("{} {}", path, e)
                        }
                    }
                }
                Err(e) => {
                    println!("{} {}", path, e)
                }
            }
        }
        Err(e) => {
            println!("{} {}", path, e)
        }
    }
}

///讀取文件內容
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 轉化為u32數字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}

天啊,雖然是實現了上面場景的需求,但是代碼猶如疊羅漢,程序結構越來越深啊,這個是我們沒法接受的!match匹配導致程序如此不堪一擊。那么有沒有第三種方法呢?當然是有的:From轉換。

前面我們說到如何自定義的Error,如何我們將上面三個error收納到我們自定義的Error中,將它們三個Error變成自定義Error子Error,這樣我們對外的Result統一返回自定義的Error。這樣程序應該可以改變點什么,我們來試試吧。

#[derive(Debug)]
enum CustomError {
    ParseIntError(std::num::ParseIntError),
    Utf8Error(std::str::Utf8Error),
    IoError(std::io::Error),
}
impl std::error::Error for CustomError{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            CustomError::IoError(ref e) => Some(e),
            CustomError::Utf8Error(ref e) => Some(e),
            CustomError::ParseIntError(ref e) => Some(e),
        }
    }
}

impl Display for CustomError{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self {
            CustomError::IoError(ref e) => e.fmt(f),
            CustomError::Utf8Error(ref e) => e.fmt(f),
            CustomError::ParseIntError(ref e) => e.fmt(f),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(s: std::num::ParseIntError) -> Self {
        CustomError::ParseIntError(s)
    }
}

impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CustomError::IoError(s)
    }
}

impl From<Utf8Error> for CustomError {
    fn from(s: std::str::Utf8Error) -> Self {
        CustomError::Utf8Error(s)
    }
}
  • CustomError為我們實現的自定義Error
  • CustomError有三個子類型Error
  • CustomError分別實現了三個子類型Error From的trait,將其類型包裝為自定義Error的子類型

好了,有了自定義的CustomError,那怎么使用呢? 我們看代碼:

use std::io::Error as IoError;
use std::str::Utf8Error;
use std::num::ParseIntError;
use std::fmt::{Display, Formatter};


fn main() -> std::result::Result<(),CustomError>{
    let path = "./dat";
    let v = read_file(path)?;
    let x = to_utf8(v.as_bytes())?;
    let u = to_u32(x)?;
    println!("num:{:?}",u);
    Ok(())
}

///讀取文件內容
fn read_file(path: &str) -> std::result::Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 轉化為u32數字
fn to_u32(v: &str) -> std::result::Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}


#[derive(Debug)]
enum CustomError {
    ParseIntError(std::num::ParseIntError),
    Utf8Error(std::str::Utf8Error),
    IoError(std::io::Error),
}
impl std::error::Error for CustomError{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            CustomError::IoError(ref e) => Some(e),
            CustomError::Utf8Error(ref e) => Some(e),
            CustomError::ParseIntError(ref e) => Some(e),
        }
    }
}

impl Display for CustomError{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self {
            CustomError::IoError(ref e) => e.fmt(f),
            CustomError::Utf8Error(ref e) => e.fmt(f),
            CustomError::ParseIntError(ref e) => e.fmt(f),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(s: std::num::ParseIntError) -> Self {
        CustomError::ParseIntError(s)
    }
}

impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CustomError::IoError(s)
    }
}

impl From<Utf8Error> for CustomError {
    fn from(s: std::str::Utf8Error) -> Self {
        CustomError::Utf8Error(s)
    }
}

其實我們主要關心的是這段代碼:

fn main() -> Result<(),CustomError>{
    let path = "./dat";
    let v = read_file(path)?;
    let x = to_utf8(v.as_bytes())?;
    let u = to_u32(x)?;
    println!("num:{:?}",u);
    Ok(())
}

我們使用了?來替代原來的match匹配的方式。?使用問號作用在函數的結束,意思是:

  • 程序接受了一個Result<(),CustomError>自定義的錯誤類型。
  • 當前如果函數結果錯誤,程序自動拋出Err自身錯誤類型,并包含相關自己類型錯誤信息,因為我們做了From轉換的操作,該函數的自身類型錯誤會通過實現的From操作自動轉化為CustomError的自定義類型錯誤。
  • 當前如果函數結果正確,繼續之后邏輯,直到程序結束。

這樣,我們通過From?解決了之前match匹配代碼層級深的問題,因為這種轉換是無感知的,使得我們在處理好錯誤類型后,只需要關心我們的目標值即可,這樣不需要顯示對Err(e)的數據單獨處理,使得我們在函數后添加?后,程序一切都是自動了。

還記得我們之前討論在對比golang的錯誤處理時的:if err!=nil的邏輯了嗎,這種因為用了?語法糖使得該段判斷將不再存在。

另外,我們還注意到,Result的結果可以作用在main函數上,

  • 是的,Result的結果不僅能作用在main函數上
  • Result還可以作用在單元測試上,這就是我們文中剛開始提到的:因為有了Result的作用,使得我們在程序中幾乎可以完全摒棄unwrap()的代碼塊,使得程序更輕,大大減少潛在問題,程序組織結構更加清晰。

下面這是作用在單元測試上的Result的代碼:

...

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_num() -> std::result::Result<(), CustomError> {
        let path = "./dat";
        let v = read_file(path)?;
        let x = to_utf8(v.as_bytes())?;
        let u = to_u32(x)?;
        assert_eq!(u, 8);
        Ok(())
    }
}

7. 重命名Result

我們在實際項目中,會大量使用如上的Result結果,并且ResultErr類型是我們自定義錯誤,導致我們寫程序時會顯得非常啰嗦、冗余

///讀取文件內容
fn read_file(path: &str) -> std::result::Result<String, CustomError> {
    let val = std::fs::read_to_string(path)?;
    Ok(val)
}

/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, CustomError> {
    let x = std::str::from_utf8(v)?;
    Ok(x)
}

/// 轉化為u32數字
fn to_u32(v: &str) -> std::result::Result<u32, CustomError> {
    let i = v.parse::<u32>()?;
    Ok(i)
}

我們的程序中,會大量充斥著這種模板代碼,Rust本身支持對類型自定義,使得我們只需要重命名Result即可:

pub type IResult<I> = std::result::Result<I, CustomError>; ///自定義Result類型:IResult

這樣,凡是使用的是自定義類型錯誤的Result都可以使用IResult來替換std::result::Result的類型,使得簡化程序,隱藏Error類型及細節,關注目標主體,代碼如下:

///讀取文件內容
fn read_file(path: &str) -> IResult<String> {
    let val = std::fs::read_to_string(path)?;
    Ok(val)
}

/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> IResult<&str> {
    let x = std::str::from_utf8(v)?;
    Ok(x)
}

/// 轉化為u32數字
fn to_u32(v: &str) -> IResult<u32> {
    let i = v.parse::<u32>()?;
    Ok(i)
}

std::result::Result<I, CustomError> 替換為:IResult<I>類型

當然,會有人提問,如果是多參數類型怎么處理呢,同樣,我們只需將OK類型變成 tuple (I,O)類型的多參數數據即可,大概這樣:

pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;

使用也及其簡單,只需要返回:I,O的具體類型,舉個示例:

fn foo() -> IResult<String, u32> {
    Ok((String::from("bar"), 32))
}

使用重命名類型的Result,使得我們錯誤類型統一,方便處理。在實際項目中,可以大量看到這種例子的存在。

8. Option轉換

我們知道,在Rust中,需要使用到unwrap()的方法的對象有Result,Option對象。我們看下Option的大致結構:

pub enum Option<T> {
    /// No value
    #[stable(feature = "rust1", since = "1.0.0")]
    None,
    /// Some value `T`
    #[stable(feature = "rust1", since = "1.0.0")]
    Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}

Option本身是一個enum對象,如果該函數(方法)調用結果值沒有值,返回None,反之有值返回Some(T)

如果我們想獲取Some(T)中的T,最直接的方式是:unwrap()。我們前面說過,使用unwrap()的方式太過于暴力,如果出錯,程序直接panic,這是我們最不愿意看到的結果。

Ok,那么我們試想下, 利用Option能使用?語法糖嗎?如果能用?轉換的話,是不是代碼結構就更簡單了呢?我們嘗試下,代碼如下:


#[derive(Debug)]
enum Error {
    OptionError(String),
}

impl std::error::Error for Error {}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self {
            Error::OptionError(ref e) => e.fmt(f),
        }
    }
}

pub type Result<I> = std::result::Result<I, Error>;


fn main() -> Result<()> {
    let bar = foo(60)?;
    assert_eq!("bar", bar);
    Ok(())
}

fn foo(index: i32) -> Option<String> {
    if index > 60 {
        return Some("bar".to_string());
    }
    None
}

執行結果報錯:

error[E0277]: `?` couldn't convert the error to `Error`
  --> src/main.rs:22:22
   |
22 |     let bar = foo(60)?;
   |                      ^ the trait `std::convert::From<std::option::NoneError>` is not implemented for `Error`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = note: required by `std::convert::From::from`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `hyper-define`.

提示告訴我們沒有轉換std::convert::From<std::option::NoneError>,但是NoneError本身是unstable,這樣我們沒法通過From轉換為自定義Error

本身,在Rust的設計中,關于OptionResult就是一對孿生兄弟一樣的存在,Option的存在可以忽略異常的細節,直接關注目標主體。當然,Option也可以通過內置的組合器ok_or()方法將其變成Result。我們大致看下實現細節:

impl<T> Option<T> {
    pub fn ok_or<E>(self, err: E) -> Result<T, E> {
        match self {
            Some(v) => Ok(v),
            None => Err(err),
        }
    }
}    

這里通過ok_or()方法通過接收一個自定義Error類型,將一個Option->Result。好的,變成Result的類型,我們就是我們熟悉的領域了,這樣處理起來就很靈活。

關于Option的其他處理方式,不在此展開解決,詳細的可看下面鏈接:

延伸鏈接:https://stackoverflow.com/questions/59568278/why-does-the-operator-report-the-error-the-trait-bound-noneerror-error-is-no

9. 避免unwrap()

有人肯定會有疑問,如果需要判斷的邏輯,又不用?這種操作,怎么取出OptionResult的數據呢,當然點子總比辦法多,我們來看下Option如何做的:

fn main() {
    if let Some(v) = opt_val(60) {
        println!("{}", v);
    }
}

fn opt_val(num: i32) -> Option<String> {
    if num >= 60 {
        return Some("foo bar".to_string());
    }
    None
}

是的,我們使用if let Some(v)的方式取出值,當前else的邏輯就可能需要自己處理了。當然,Option可以這樣做,Result也一定可以:

fn main() {
    if let Ok(v) = read_file("./dat") {
        println!("{}", v);
    }
}

fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

只不過,在處理Result的判斷時,使用的是if let Ok(v),這個和Optionif let Some(v)有所不同。

到這里,unwrap()的代碼片在項目中應該可以規避了。補充下,這里強調了幾次規避,就如前所言:團隊風格統一,方便管理代碼,消除潛在危機

10. 自定義Error同級轉換

我們在項目中,一個函數(方法)內部會有多次Result的結果判斷:?,假設我們自定義的全局Error名稱為:GlobalError。

這時候,如果全局有一個Error可能就會出現如下錯誤:

std::convert::From<error::GlobalError<A>>` is not implemented for `error::GlobalError<B>

意思是:我們自定義的GlobalError沒有通過From<GlobalError<T>>轉換我們自己自定義的GlobalError,那這樣,就等于自己轉換自己。注意:

  • 第一:這是我們不期望這樣做的。
  • 第二:遇到這種自己轉換自己的T類型很多,我們不可能把出現的T類型通通實現一遍。
    這時候,我們考慮自定義另一個Error了,假設我們視為:InnnerError,我們全局的Error取名為:GlobalError,我們在遇到上面錯誤時,返回Result<T,InnerError>,這樣我們遇到Result<T,GlobalError>時,只需要通過From<T>轉換即可,代碼示例如下:
impl From<InnerError> for GlobalError {
    fn from(s: InnerError) -> Self {
        Error::new(ErrorKind::InnerError(e))
    }
}

上面說的這種情況,可能會在項目中出現多個自定義Error,出現這種情況時,存在多個不同Error的std::result::Result<T,Err>的返回。這里的Err就可以根據我們業務現狀分別反回不同類型了。最終,只要實現了From<T>trait可轉化為最終期望結果。

11. Error常見開源庫

好了,介紹到這里,我們應該有了非常清晰的認知:關于如何處理Rust的錯誤處理問題了。但是想想上面的這些邏輯多數是模板代碼,我們在實際中,大可不必這樣。說到這里,開源社區也有了很多對錯誤處理庫的支持,下面列舉了一些:

12. 參考鏈接

13 錯誤處理實戰

這個例子介紹了如何在https://github.com/Geal/nom中處理錯誤,這里就不展開介紹了,有興趣的可自行閱讀代碼。

詳細見鏈接:https://github.com/baoyachi/rust-error-handle/blob/master/src/demo_nom_error_handle.rs

14. 總結

好了,經過上面的長篇大論,不知道大家是否明白如何自定義處理Error呢了。大家現在帶著之前的已有的問題或困惑,趕緊實戰下Rust的錯誤處理吧,大家有疑問或者問題都可以留言我,希望這篇文章對你有幫助。

文中代碼詳見:https://github.com/baoyachi/rust-handle-error/tree/master/src

原文地址:https://github.com/baoyachi/rust-error-handle

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • github地址:https://github.com/bradyjoestar/rustnotes(歡迎star...
    bradyjoestar閱讀 1,504評論 0 0
  • 簡介 Rust 是最近幾年開始興起的編程語言,雖然目前還沒看到要像 Go 一樣”大火“的趨勢。但是,官網的一些 f...
    linjinhe閱讀 6,489評論 1 15
  • 如果你已經開始學習Rust,相信你已經體會過Rust編譯器的強大。它可以幫助你避免程序中的大部分錯誤,但是編譯器也...
    Jackeyzhe閱讀 766評論 0 0
  • Error Handing with Golang Go errors are values.Naming:Err...
    吃豬的蛇閱讀 749評論 1 7
  • error code(錯誤代碼)=2000是無效的像素格式。error code(錯誤代碼)=2001是指定的驅動...
    Heikki_閱讀 1,874評論 0 4