細(xì)說(shuō)Rust錯(cuò)誤處理

image.png

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

1. 前言

這篇文章寫得比較長(zhǎng)鞍陨,全文讀完大約需要15-20min输钩,如果對(duì)Rust的錯(cuò)誤處理不清楚或還有些許模糊的同學(xué),請(qǐng)靜下心來(lái)細(xì)細(xì)閱讀大刊。當(dāng)讀完該篇文章后,可以說(shuō)對(duì)Rust的錯(cuò)誤處理可以做到掌握自如。

筆者花費(fèi)較長(zhǎng)篇幅來(lái)描述錯(cuò)誤處理的來(lái)去扩劝,詳細(xì)介紹其及一步步梳理內(nèi)容,望大家能耐心讀完后對(duì)大家有所幫助职辅。當(dāng)然棒呛,在寫這篇文章之時(shí),也借閱了大量互聯(lián)網(wǎng)資料域携,詳見鏈接見底部參考鏈接

掌握好Rust的錯(cuò)誤設(shè)計(jì)簇秒,不僅可以提升我們對(duì)錯(cuò)誤處理的認(rèn)識(shí),對(duì)代碼結(jié)構(gòu)秀鞭、層次都有很大的幫助趋观。那廢話不多說(shuō),那我們開啟這段閱讀之旅吧??锋边!

2. 背景

筆者在寫這篇文章時(shí)皱坛,也翻閱一些資料關(guān)于Rust的錯(cuò)誤處理資料,多數(shù)是對(duì)其一筆帶過(guò)宠默,導(dǎo)致之前接觸過(guò)其他語(yǔ)言的新同學(xué)來(lái)說(shuō)麸恍,上手處理Rust的錯(cuò)誤會(huì)有當(dāng)頭棒喝的感覺(jué)。找些資料發(fā)現(xiàn)unwrap()也可以解決問(wèn)題搀矫,然后心中暗自竊喜抹沪,程序在運(yùn)行過(guò)程中,因?yàn)楹雎詸z查或程序邏輯判斷瓤球,導(dǎo)致某些情況融欧,程序panic。這可能是我們最不愿看到的現(xiàn)象卦羡,遂又回到起點(diǎn)噪馏,重新去了解Rust的錯(cuò)誤處理。

這篇文章绿饵,通過(guò)一步步介紹欠肾,讓大家清晰知道Rust的錯(cuò)誤處理的究竟。介紹在Rust中的錯(cuò)誤使用及如何處理錯(cuò)誤拟赊,以及在實(shí)際工作中關(guān)于其使用技巧刺桃。

3. unwrap的危害!

下面我們來(lái)看一段代碼,執(zhí)行一下:

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

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

程序執(zhí)行結(jié)果:

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.

什么,因?yàn)?code>path路徑不對(duì)吸祟,程序竟然崩潰了瑟慈,這個(gè)是我們不能接受的桃移!

unwrap() 這個(gè)操作在rust代碼中,應(yīng)該看過(guò)很多這種代碼葛碧,甚至此時(shí)我們正在使用它借杰。它主要用于OptionResult的打開其包裝的結(jié)果。常常我們?cè)诖a中进泼,使用簡(jiǎn)單蔗衡,或快速處理,使用了 unwrap() 的操作缘琅,但是粘都,它是一個(gè)非常危險(xiǎn)的信號(hào)!

可能因?yàn)?strong>沒(méi)有程序檢查或校驗(yàn),潛在的bug可能就出現(xiàn)其中刷袍,使得我們程序往往就panic了。這可能使我們最不愿看到的現(xiàn)象樊展。

在實(shí)際項(xiàng)目開發(fā)中呻纹,程序中可能充斥著大量代碼,我們很難避免unwrap()的出現(xiàn)专缠,為了解決這種問(wèn)題雷酪,我們通過(guò)做code review,或使用腳本工具檢查其降低其出現(xiàn)的可能性。

通常每個(gè)項(xiàng)目都有一些約束涝婉,或許:在大型項(xiàng)目開發(fā)中哥力, 不用unwrap() 方法,使用其他方式處理程序墩弯,unwrap() 的不出現(xiàn)可能會(huì)使得程序的健壯性高出很多吩跋。

這里前提是團(tuán)隊(duì)或大型項(xiàng)目,如果只是寫一個(gè)簡(jiǎn)單例子(demo)就不在本篇文章的討論范疇渔工。因?yàn)橐粋€(gè)Demo的問(wèn)題锌钮,可能只是快速示范或演示,不考慮程序健壯性, unwrap() 的操作可能會(huì)更方便代碼表達(dá)引矩。

可能有人會(huì)問(wèn)梁丘,我們通常跑程序unit test,其中的很多mock數(shù)據(jù)會(huì)有 unwrap() 的操作旺韭,我們只是為了在單元測(cè)試中使得程序簡(jiǎn)單氛谜。這種也能不使用嗎?答案:是的区端,完全可以不使用 unwrap() 也可以做到的值漫。

4. 對(duì)比語(yǔ)言處理錯(cuò)誤

說(shuō)到unwrap(),我們不得不提到rust的錯(cuò)誤處理珊燎,unwrap()Rust的錯(cuò)誤處理是密不可分的惭嚣。

4.1 golang的錯(cuò)誤處理演示

如果了解golang的話遵湖,應(yīng)該清楚下面這段代碼的意思:

package main

import (
    "io/ioutil"
    "log"
)

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

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

我們執(zhí)行下程序,打印如下晚吞。執(zhí)行錯(cuò)誤延旧,當(dāng)然,因?yàn)槲覀兘o的文件路徑不存在槽地,程序報(bào)錯(cuò)迁沫。

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

這里,golang采用多返回值方式捌蚊,程序報(bào)錯(cuò)返回錯(cuò)誤問(wèn)題集畅,通過(guò)判斷 err!=nil 來(lái)決定程序是否繼續(xù)執(zhí)行或終止該邏輯。當(dāng)然缅糟,如果接觸過(guò)golang項(xiàng)目時(shí)挺智,會(huì)發(fā)現(xiàn)程序中大量充斥著if err!=nil的代碼,對(duì)此網(wǎng)上有對(duì)if err!=nil進(jìn)行了很多討論窗宦,因?yàn)檫@個(gè)不在本篇文章的范疇中赦颇,在此不對(duì)其追溯、討論赴涵。

4.2 Rust 錯(cuò)誤處理示例

對(duì)比了golang代碼媒怯,我們對(duì)照上面的例子,看下在Rust中如何編寫這段程序髓窜,代碼如下:

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

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

當(dāng)前寄纵,因?yàn)槲覀兘o的文件路徑不存在鳖敷,程序報(bào)錯(cuò),打印內(nèi)容如下:

No such file or directory (os error 2)

Rust代表中擂啥,Result是一個(gè)enum枚舉對(duì)象,部分源碼如下:

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的枚舉對(duì)象作為程序的返回值哄陶,通過(guò)Result來(lái)判斷其結(jié)果,我們使用match匹配的方式來(lái)獲取Result的內(nèi)容哺壶,判斷正常(Ok)或錯(cuò)誤(Err)屋吨。

或許,我們大致向上看去山宾,golang代碼和Rust代碼沒(méi)有本質(zhì)區(qū)別至扰,都是采用返回值方式,給出程序結(jié)果资锰。下面我們就對(duì)比兩種語(yǔ)言說(shuō)說(shuō)之間區(qū)別:

  • golang采用多返回值方式敢课,我們?cè)谀玫侥繕?biāo)結(jié)果時(shí)(上面是指文件內(nèi)容file),需要首先對(duì)err判斷是否為nil,并且我們?cè)?code>return時(shí),需要給多返回值分別賦值直秆,調(diào)用時(shí)需要對(duì) if err!=nil 做結(jié)果判斷濒募。
  • Rust中采用Result的枚舉對(duì)象做結(jié)果返回。枚舉的好處是:多選一圾结。因?yàn)?code>Result的枚舉類型為OkErr瑰剃,使得我們每次在返回Result的結(jié)果時(shí),要么是Ok,要么是Err筝野。它不需要return結(jié)果同時(shí)給兩個(gè)值賦值晌姚,這樣的情況只會(huì)存在一種可能性: Ok or Err
  • golang的函數(shù)調(diào)用需要對(duì) if err!=nil做結(jié)果判斷歇竟,因?yàn)檫@段代碼 判斷是手動(dòng)邏輯挥唠,往往我們可能因?yàn)槭韬觯瑢?dǎo)致這段邏輯缺失焕议,缺少校驗(yàn)宝磨。當(dāng)然,我們?cè)诰帉懘a期間可以通過(guò)某些工具 lint 掃描出這種潛在bug盅安。
  • Rustmatch判斷是自動(dòng)打開懊烤,當(dāng)然你也可以選擇忽略其中某一個(gè)枚舉值,我們不在此說(shuō)明。

可能有人發(fā)現(xiàn)宽堆,如果我有多個(gè)函數(shù),需要多個(gè)函數(shù)的執(zhí)行結(jié)果茸习,這樣需要match代碼多次畜隶,代碼會(huì)不會(huì)是一坨一坨,顯得代碼很臃腫号胚,難看籽慢。是的,這個(gè)問(wèn)題提出的的確是有這種問(wèn)題猫胁,不過(guò)這個(gè)在后面我們講解的時(shí)候箱亿,會(huì)通過(guò)程序語(yǔ)法糖避免多次match多次結(jié)果的問(wèn)題,不過(guò)我們?cè)诖讼炔粩⒄f(shuō)弃秆,后面將有介紹届惋。

5. Rust中的錯(cuò)誤處理

前面不管是golang還是Rust采用return返回值方式,兩者都是為了解決程序中錯(cuò)誤處理的問(wèn)題菠赚。好了脑豹,前面說(shuō)了這么多,我們還是回歸正題:Rust中是如何對(duì)錯(cuò)誤進(jìn)行處理的衡查?

要想細(xì)致了解Rust的錯(cuò)誤處理瘩欺,我們需要了解std::error::Error,該trait的內(nèi)部方法,部分代碼如下:
參考鏈接: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()在文檔介紹中俱饿,盡管使用它不會(huì)導(dǎo)致編譯警告歌粥,但新代碼應(yīng)該實(shí)現(xiàn)impl Display ,新impl的可以省略拍埠,不用實(shí)現(xiàn)該方法, 要獲取字符串形式的錯(cuò)誤描述失驶,請(qǐng)使用to_string()

  • cause()1.33.0被拋棄械拍,取而代之使用source()方法突勇,新impl的不用實(shí)現(xiàn)該方法。

  • source()此錯(cuò)誤的低級(jí)源坷虑,如果內(nèi)部有錯(cuò)誤類型Err返回:Some(e),如果沒(méi)有返回:None甲馋。

    • 如果當(dāng)前Error是低級(jí)別的Error,并沒(méi)有子Error,需要返回None。介于其本身默認(rèn)有返回值None迄损,可以不覆蓋該方法定躏。
    • 如果當(dāng)前Error包含子Error,需要返回子ErrorSome(err),需要覆蓋該方法。
  • type_id()該方法被隱藏芹敌。

  • backtrace()返回發(fā)生此錯(cuò)誤的堆棧追溯痊远,因?yàn)闃?biāo)記unstable,在Ruststable版本不被使用氏捞。

  • 自定義的Error需要impl std::fmt::Debug的trait,當(dāng)然我們只需要在默認(rèn)對(duì)象上添加注解:#[derive(Debug)]即可碧聪。

總結(jié)一下,自定義一個(gè)error需要實(shí)現(xiàn)如下幾步:

  • 手動(dòng)實(shí)現(xiàn)impl std::fmt::Display的trait,并實(shí)現(xiàn) fmt(...)方法液茎。
  • 手動(dòng)實(shí)現(xiàn)impl std::fmt::Debugtrait逞姿,一般直接添加注解即可:#[derive(Debug)]
  • 手動(dòng)實(shí)現(xiàn)impl std::error::Errortrait,并根據(jù)自身error級(jí)別是否覆蓋std::error::Error中的source()方法。

下面我們自己手動(dòng)實(shí)現(xiàn)下Rust自定義錯(cuò)誤:CustomError

use std::error::Error;

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

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

///實(shí)現(xiàn)Error的trait,因?yàn)橛凶覧rror:ChildError,需要覆蓋source()方法,返回Some(err)
impl std::error::Error for CustomError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.err)
    }
}


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

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

///實(shí)現(xiàn)Error的trait,因?yàn)闆](méi)有子Error,不需要覆蓋source()方法
impl std::error::Error for ChildError {}

///構(gòu)建一個(gè)Result的結(jié)果,返回自定義的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,沒(méi)有覆蓋source()方法栋烤,空實(shí)現(xiàn)了std::error::Error
  • CustomError有子類型ChildError,覆蓋source(),并返回了子類型Option值:Some(&self.err)

運(yùn)行執(zhí)行結(jié)果谒养,顯示如下:

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

至此,我們就了解了如何實(shí)現(xiàn)Rust自定義Error了明郭。

6. 自定義Error轉(zhuǎn)換:From

上面我們說(shuō)到买窟,函數(shù)返回Result的結(jié)果時(shí),需要獲取函數(shù)的返回值是成功(Ok)還是失敗(Err)达址,需要使用match匹配蔑祟,我們看下多函數(shù)之間調(diào)用是如何解決這類問(wèn)題的?假設(shè)我們有個(gè)場(chǎng)景:

  • 讀取一文件
  • 將文件內(nèi)容轉(zhuǎn)化為UTF8格式
  • 將轉(zhuǎn)換后格式內(nèi)容轉(zhuǎn)為u32的數(shù)字沉唠。

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

...

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

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

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

最終疆虚,我們得到u32的數(shù)字,對(duì)于該場(chǎng)景如何組織我們代碼呢?

  • unwrap()直接打開三個(gè)方法径簿,取出值罢屈。這種方式太暴力,并且會(huì)有bug,造成程序panic,不被采納篇亭。
  • match匹配缠捌,如何返回OK,繼續(xù)下一步,否則報(bào)錯(cuò)終止邏輯译蒂,那我們?cè)囋嚒?/li>

參考代碼如下:

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

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

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

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

天啊曼月,雖然是實(shí)現(xiàn)了上面場(chǎng)景的需求,但是代碼猶如疊羅漢柔昼,程序結(jié)構(gòu)越來(lái)越深啊哑芹,這個(gè)是我們沒(méi)法接受的!match匹配導(dǎo)致程序如此不堪一擊捕透。那么有沒(méi)有第三種方法呢聪姿?當(dāng)然是有的:From轉(zhuǎn)換。

前面我們說(shuō)到如何自定義的Error,如何我們將上面三個(gè)error收納到我們自定義的Error中乙嘀,將它們?nèi)齻€(gè)Error變成自定義Error子Error末购,這樣我們對(duì)外的Result統(tǒng)一返回自定義的Error。這樣程序應(yīng)該可以改變點(diǎn)什么虎谢,我們來(lái)試試吧盟榴。

#[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為我們實(shí)現(xiàn)的自定義Error
  • CustomError有三個(gè)子類型Error
  • CustomError分別實(shí)現(xiàn)了三個(gè)子類型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(())
}

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

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

/// 轉(zhuǎn)化為u32數(shù)字
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)
    }
}

其實(shí)我們主要關(guān)心的是這段代碼:

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(())
}

我們使用了?來(lái)替代原來(lái)的match匹配的方式曹货。?使用問(wèn)號(hào)作用在函數(shù)的結(jié)束,意思是:

  • 程序接受了一個(gè)Result<(),CustomError>自定義的錯(cuò)誤類型讳推。
  • 當(dāng)前如果函數(shù)結(jié)果錯(cuò)誤,程序自動(dòng)拋出Err自身錯(cuò)誤類型玩般,并包含相關(guān)自己類型錯(cuò)誤信息银觅,因?yàn)槲覀冏隽?code>From轉(zhuǎn)換的操作,該函數(shù)的自身類型錯(cuò)誤會(huì)通過(guò)實(shí)現(xiàn)的From操作自動(dòng)轉(zhuǎn)化為CustomError的自定義類型錯(cuò)誤坏为。
  • 當(dāng)前如果函數(shù)結(jié)果正確究驴,繼續(xù)之后邏輯,直到程序結(jié)束匀伏。

這樣洒忧,我們通過(guò)From?解決了之前match匹配代碼層級(jí)深的問(wèn)題,因?yàn)檫@種轉(zhuǎn)換是無(wú)感知的够颠,使得我們?cè)谔幚砗缅e(cuò)誤類型后熙侍,只需要關(guān)心我們的目標(biāo)值即可,這樣不需要顯示對(duì)Err(e)的數(shù)據(jù)單獨(dú)處理,使得我們?cè)诤瘮?shù)后添加?后蛉抓,程序一切都是自動(dòng)了庆尘。

還記得我們之前討論在對(duì)比golang的錯(cuò)誤處理時(shí)的:if err!=nil的邏輯了嗎,這種因?yàn)橛昧?code>?語(yǔ)法糖使得該段判斷將不再存在巷送。

另外驶忌,我們還注意到,Result的結(jié)果可以作用在main函數(shù)上笑跛,

  • 是的付魔,Result的結(jié)果不僅能作用在main函數(shù)上
  • Result還可以作用在單元測(cè)試上,這就是我們文中剛開始提到的:因?yàn)橛辛?code>Result的作用飞蹂,使得我們?cè)诔绦蛑袔缀蹩梢酝耆饤?code>unwrap()的代碼塊几苍,使得程序更輕,大大減少潛在問(wèn)題晤柄,程序組織結(jié)構(gòu)更加清晰擦剑。

下面這是作用在單元測(cè)試上的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

我們?cè)趯?shí)際項(xiàng)目中,會(huì)大量使用如上的Result結(jié)果芥颈,并且ResultErr類型是我們自定義錯(cuò)誤,導(dǎo)致我們寫程序時(shí)會(huì)顯得非常啰嗦惠勒、冗余

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

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

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

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

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

這樣,凡是使用的是自定義類型錯(cuò)誤的Result都可以使用IResult來(lái)替換std::result::Result的類型盾计,使得簡(jiǎn)化程序售担,隱藏Error類型及細(xì)節(jié),關(guān)注目標(biāo)主體署辉,代碼如下:

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

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

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

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

當(dāng)然族铆,會(huì)有人提問(wèn),如果是多參數(shù)類型怎么處理呢哭尝,同樣哥攘,我們只需將OK類型變成 tuple (I,O)類型的多參數(shù)數(shù)據(jù)即可,大概這樣:

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

使用也及其簡(jiǎn)單材鹦,只需要返回:I,O的具體類型,舉個(gè)示例:

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

使用重命名類型的Result逝淹,使得我們錯(cuò)誤類型統(tǒng)一,方便處理桶唐。在實(shí)際項(xiàng)目中栅葡,可以大量看到這種例子的存在。

8. Option轉(zhuǎn)換

我們知道尤泽,在Rust中欣簇,需要使用到unwrap()的方法的對(duì)象有Result,Option對(duì)象规脸。我們看下Option的大致結(jié)構(gòu):

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本身是一個(gè)enum對(duì)象,如果該函數(shù)(方法)調(diào)用結(jié)果值沒(méi)有值醉蚁,返回None,反之有值返回Some(T)

如果我們想獲取Some(T)中的T,最直接的方式是:unwrap()燃辖。我們前面說(shuō)過(guò),使用unwrap()的方式太過(guò)于暴力网棍,如果出錯(cuò)黔龟,程序直接panic,這是我們最不愿意看到的結(jié)果滥玷。

Ok,那么我們?cè)囅胂? 利用Option能使用?語(yǔ)法糖嗎氏身?如果能用?轉(zhuǎn)換的話,是不是代碼結(jié)構(gòu)就更簡(jiǎn)單了呢惑畴?我們嘗試下,代碼如下:


#[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
}

執(zhí)行結(jié)果報(bào)錯(cuò):

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`.

提示告訴我們沒(méi)有轉(zhuǎn)換std::convert::From<std::option::NoneError>蛋欣,但是NoneError本身是unstable,這樣我們沒(méi)法通過(guò)From轉(zhuǎn)換為自定義Error如贷。

本身陷虎,在Rust的設(shè)計(jì)中,關(guān)于OptionResult就是一對(duì)孿生兄弟一樣的存在杠袱,Option的存在可以忽略異常的細(xì)節(jié)尚猿,直接關(guān)注目標(biāo)主體。當(dāng)然楣富,Option也可以通過(guò)內(nèi)置的組合器ok_or()方法將其變成Result凿掂。我們大致看下實(shí)現(xiàn)細(xì)節(jié):

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

這里通過(guò)ok_or()方法通過(guò)接收一個(gè)自定義Error類型,將一個(gè)Option->Result纹蝴。好的庄萎,變成Result的類型,我們就是我們熟悉的領(lǐng)域了塘安,這樣處理起來(lái)就很靈活糠涛。

關(guān)于Option的其他處理方式,不在此展開解決兼犯,詳細(xì)的可看下面鏈接:

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

9. 避免unwrap()

有人肯定會(huì)有疑問(wèn)脱羡,如果需要判斷的邏輯,又不用?這種操作免都,怎么取出OptionResult的數(shù)據(jù)呢,當(dāng)然點(diǎn)子總比辦法多帆竹,我們來(lái)看下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)的方式取出值,當(dāng)前else的邏輯就可能需要自己處理了栽连。當(dāng)然险领,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)
}

只不過(guò),在處理Result的判斷時(shí)绢陌,使用的是if let Ok(v)挨下,這個(gè)和Optionif let Some(v)有所不同。

到這里脐湾,unwrap()的代碼片在項(xiàng)目中應(yīng)該可以規(guī)避了臭笆。補(bǔ)充下,這里強(qiáng)調(diào)了幾次規(guī)避秤掌,就如前所言:團(tuán)隊(duì)風(fēng)格統(tǒng)一愁铺,方便管理代碼,消除潛在危機(jī)闻鉴。

10. 自定義Error同級(jí)轉(zhuǎn)換

我們?cè)陧?xiàng)目中茵乱,一個(gè)函數(shù)(方法)內(nèi)部會(huì)有多次Result的結(jié)果判斷:?,假設(shè)我們自定義的全局Error名稱為:GlobalError

這時(shí)候孟岛,如果全局有一個(gè)Error可能就會(huì)出現(xiàn)如下錯(cuò)誤:

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

意思是:我們自定義的GlobalError沒(méi)有通過(guò)From<GlobalError<T>>轉(zhuǎn)換我們自己自定義的GlobalError瓶竭,那這樣,就等于自己轉(zhuǎn)換自己渠羞。注意:

  • 第一:這是我們不期望這樣做的斤贰。
  • 第二:遇到這種自己轉(zhuǎn)換自己的T類型很多,我們不可能把出現(xiàn)的T類型通通實(shí)現(xiàn)一遍堵未。
    這時(shí)候腋舌,我們考慮自定義另一個(gè)Error了,假設(shè)我們視為:InnnerError,我們?nèi)值腅rror取名為:GlobalError渗蟹,我們?cè)谟龅缴厦驽e(cuò)誤時(shí)块饺,返回Result<T,InnerError>,這樣我們遇到Result<T,GlobalError>時(shí),只需要通過(guò)From<T>轉(zhuǎn)換即可雌芽,代碼示例如下:
impl From<InnerError> for GlobalError {
    fn from(s: InnerError) -> Self {
        Error::new(ErrorKind::InnerError(e))
    }
}

上面說(shuō)的這種情況授艰,可能會(huì)在項(xiàng)目中出現(xiàn)多個(gè)自定義Error,出現(xiàn)這種情況時(shí),存在多個(gè)不同Error的std::result::Result<T,Err>的返回世落。這里的Err就可以根據(jù)我們業(yè)務(wù)現(xiàn)狀分別反回不同類型了淮腾。最終,只要實(shí)現(xiàn)了From<T>trait可轉(zhuǎn)化為最終期望結(jié)果屉佳。

11. Error常見開源庫(kù)

好了谷朝,介紹到這里,我們應(yīng)該有了非常清晰的認(rèn)知:關(guān)于如何處理Rust的錯(cuò)誤處理問(wèn)題了武花。但是想想上面的這些邏輯多數(shù)是模板代碼圆凰,我們?cè)趯?shí)際中,大可不必這樣体箕。說(shuō)到這里专钉,開源社區(qū)也有了很多對(duì)錯(cuò)誤處理庫(kù)的支持挑童,下面列舉了一些:

12. 參考鏈接

13 錯(cuò)誤處理實(shí)戰(zhàn)

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

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

14. 總結(jié)

好了,經(jīng)過(guò)上面的長(zhǎng)篇大論菇民,不知道大家是否明白如何自定義處理Error呢了尽楔。大家現(xiàn)在帶著之前的已有的問(wèn)題或困惑,趕緊實(shí)戰(zhàn)下Rust的錯(cuò)誤處理吧玉雾,大家有疑問(wèn)或者問(wèn)題都可以留言我翔试,希望這篇文章對(duì)你有幫助。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末复旬,一起剝皮案震驚了整個(gè)濱河市垦缅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驹碍,老刑警劉巖壁涎,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異志秃,居然都是意外死亡怔球,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門浮还,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)竟坛,“玉大人,你說(shuō)我怎么就攤上這事钧舌〉L溃” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵洼冻,是天一觀的道長(zhǎng)崭歧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)撞牢,這世上最難降的妖魔是什么率碾? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮屋彪,結(jié)果婚禮上所宰,老公的妹妹穿的比我還像新娘。我一直安慰自己畜挥,他們只是感情好仔粥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著砰嘁,像睡著了一般件炉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上矮湘,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天斟冕,我揣著相機(jī)與錄音,去河邊找鬼缅阳。 笑死磕蛇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的十办。 我是一名探鬼主播秀撇,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼向族!你這毒婦竟也來(lái)了呵燕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤件相,失蹤者是張志新(化名)和其女友劉穎再扭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夜矗,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泛范,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了紊撕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罢荡。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖对扶,靈堂內(nèi)的尸體忽然破棺而出区赵,到底是詐尸還是另有隱情,我是刑警寧澤辩稽,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布惧笛,位于F島的核電站,受9級(jí)特大地震影響逞泄,放射性物質(zhì)發(fā)生泄漏患整。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一喷众、第九天 我趴在偏房一處隱蔽的房頂上張望各谚。 院中可真熱鬧,春花似錦到千、人聲如沸昌渤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)膀息。三九已至般眉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間潜支,已是汗流浹背甸赃。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冗酿,地道東北人埠对。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像裁替,于是被迫代替她去往敵國(guó)和親项玛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容