首頁>技術>

錯誤處理是程式設計語言中的重要組成部分,是程式開發工作中最重要,也是最容易出問題的地方之一。語言的錯誤處理機制體現了該語言的特點。

錯誤處理主要分為以下幾種

1. 使用全域性錯誤來作為錯誤處理

2. 使用返回值做為錯誤處理

3. 使用異常來做錯誤處理

4. 使用範疇論中的Mond

下面將大概介紹這4種方式,再介紹Rust的錯誤處理的特殊性

1. 使用全域性錯誤來作為錯誤處理

c語言採用了這種方式,此種方式當錯誤發生時,函式呼叫會返回NULL, 錯誤原因會記錄到 全域性變數errno中。

#include <stdio.h>#include <errno.h>#include <string.h>extern int errno ;int main (){    FILE * pf;    int errnum;    pf = fopen ("unexist.txt", "rb");    if (pf == NULL)    {        errnum = errno;        fprintf(stderr, "errno: %d\n", errno);        fprintf(stderr, "open file error: %s\n", strerror( errnum ));    }    else    {        fclose (pf);    }    return 0;}

此種方式的缺點非常明顯,使用全域性變數errno記錄錯誤原因,很容易引起問題。

2. 使用返回值做為錯誤處理

Go 語言採用了此種方式

func CopyFile(dstName, srcName string) (written int64, err error) {    src, err := os.Open(srcName)    if err != nil {        return    }    dst, err := os.Create(dstName)    if err != nil {        return    }    written, err = io.Copy(dst, src)    dst.Close()    src.Close()    return}

Go需要在每個函式呼叫的地方判斷是否有 err = nil, 這樣的好處是每個函式的錯誤都可以被處理。但缺點也是非常明顯,程式碼中大量

充斥著判斷err的程式碼,導致程式碼非常醜陋,而且可能很多地方基本上不需要關注錯誤,只需要在最頂層處理錯誤,而不是在呼叫鏈上每個地方判斷

3.使用異常

java/c#等採用了這種機制

public float divide(int a, int b) {    if(b == 0) {        throw new IllegalArgumentException("b can't be 0");    }    return a/b;}

這種方式的優點是,只要有異常,上層呼叫如果不關心異常,則可不需要處理,如果關心異常則可以捕獲異常做相應的業務處理,非常的靈活。

缺點是,呼叫者如果不看方法簽名註釋或者原始碼,則不會知道該方法是否會丟擲異常,一旦忘了處理該異常,則有可能會產生bug.

4. 使用範疇論中的Mond

Haskell使用這種方式,該模式使用ADT來封裝錯誤,將錯誤包裹到容器中

divBy :: Integral a => a -> [a] -> Maybe [a]divBy _ [] = Just []divBy _ (0:_) = NothingdivBy numerator (denom:xs) =    case divBy numerator xs of      Nothing -> Nothing      Just results -> Just ((numerator `div` denom) : results)

呼叫方在呼叫divBy函式時,需要對結果進行模式匹配。這種方式是顯式的錯誤處理,呼叫者必選處理錯誤,錯誤被包裹到Mond中,

使用Mond的一序列運算子來處理錯誤。缺點是丟失了原始的錯誤位置資訊,錯誤需要在Mond中處理,需要程式碼架構上對Mond友好。

Rust中的錯誤處理方式

Rust是一門多正規化的語言,錯誤處理吸收了Haskell, Scala的特點。Rust中的錯誤也是包裹到Mond中,不同的是錯誤不需要做模式匹配就可以從Mond中取出,

另外Rust的錯誤處理設計還兼具異常設計的特點,呼叫方如果不關注錯誤,則可以像java中向上將異常冒泡。

Rust錯誤處理的核心是std::result::Result

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

Rust中,可以使用模式匹配來處理錯誤,也可以從Result中取出資料或錯誤

fn main(){    let cost = get_cost(true, false);    match cost {        Ok(price) => println!("price is {}", price),        Err(err) => println!("{}", err)    }       
//或者取出錯誤if cost.is_ok()  {    println!("price is {}", cost.unwrap());}else {    println!("{}", cost.err().unwrap());}
}fn get_cost(num_err:bool, price_err:bool) -> Result<u32, String> {    let number = get_number(num_err);    match number {        Ok(n) => {            let price = get_price(price_err);            match price {                Ok(p) => Ok(n * p),                Err(err) => Err(err)            }        },        Err(err) => Err(err)    }}fn get_number(err:bool)-> Result<u32, String> {    if !err {        Ok(100)    } else {        Err("Failed to get num".to_string())    }}fn get_price(err:bool) ->  Result<u32, String> {    if !err {        Ok(100)    } else {        Err("Failed to get price".to_string())    }}

有沒有發現,上面的程式碼比較繁瑣,模式匹配導致容易產生大量的巢狀程式碼,unwrap是一種不夠優雅的方式,如果code review不夠仔細,可能一些unwrap會導致致命異常。

幸運的是Rust提供了try!宏,使用該宏可以讓異常提前返回,效果上類似java異常。rust也支援了try!宏的語法糖?, 大大方便了程式的編寫,以下使用?來重寫上面的例子。

必須注意的是使用try!(?)的關鍵是函式返回值型別必須是std::result::Result<T,E>

fn main() -> Result<(), String> {    let cost = get_cost(true, false)?;//這裡如果返回Err,則函式會提前返回    println!("cost is {}", cost);    Ok(())}fn get_cost(num_err:bool, price_err:bool) -> Result<u32, String> {    let number = get_number(num_err)?;//這裡如果返回Err,則函式會提前返回    let price = get_price(price_err)?;//這裡如果返回Err,則函式會提前返回    Ok(number * price)}fn get_number(err:bool)-> Result<u32, String> {    if err {        Ok(100)    } else {        Err("Failed to get price".to_string())    }}fn get_price(err:bool) ->  Result<u32, String> {    if err {        Ok(100)    } else {        Err("Failed to get price".to_string())    }}

Rust的異常體系中 std::error::Error,是一個核心Trait, 任何實現了該Trait的struct, 標準庫都將自動為其實現From, 這樣該struct就可以轉換為Box<Error + 'a>,如下

impl<'a, E: Error + 'a> From<E> for Box<Error + 'a>

也就是說,如果某個struct, MyError實現了std::error::Error,並且某個方法返回型別為 std::result::Result<T1, MyError>, 且呼叫者返回的型別為

std::result::Result<T1, Box<std::error::Error>>,則呼叫方也可以使用try!宏,還是之前的例子,稍微改動下

#[derive(Debug)]struct MyError{    msg:String}impl Display for MyError{ //實現 Error 必須先實現 Display    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.msg)    }}impl Error for MyError{}fn main() -> Result<(), Box<dyn Error>> {    let cost = get_cost(true, false)?;    println!("cost is {}", cost);    Ok(())}fn get_cost(num_err:bool, price_err:bool) -> Result<u32, Box<dyn Error>> {    let number = get_number(num_err)?;// MyError實現了 Error trait, 標準庫又為實現了Error trait的struct,實現了impl From for Box<dyn Error>,     let price = get_price(price_err)?;// 這樣就可使用try!了     Ok(number * price)}fn get_number(err:bool)-> Result<u32, MyError> {    if err {        Ok(100)    } else {        Err(MyError{msg:"Failed to get price".to_string()})    }}fn get_price(err:bool) ->  Result<u32, MyError> {    if !err {        Ok(100)    } else {        Err(MyError{msg:"Failed to get price".to_string()})    }}

可以看到 std::error:::Error充當了 java/c#中異常基類的作用,任何實現了Error的struct/enum, 方法的返回值只要宣告為Result<T, MyError>,且呼叫者的方法返回值也是Result<T, Box<dyn Error>>,則呼叫者可以使用try!或?語法糖使異常提前返回。

在實戰中,一搬會定義一個業務錯誤類 BussinessError,實現Error trait, 然後所有的方法返回 Result<T, BussinessError>,這樣所有的方法都可以使用?語法糖了。當然當呼叫第三方類庫或者呼叫標準庫時,需要將對應的異常轉為BussinessError。

如果覺得自己定義異常基類比較繁瑣,可以使用第三方類庫,比如anyhow, 該類庫定義了很多異常模式,可以直接拿來使用,以下是使用anyhow改寫後的例子

fn main() -> anyhow::Result<()> {    let cost = get_cost(true, false)?;    println!("cost is {}", cost);    Ok(())}fn get_cost(num_err:bool, price_err:bool) -> anyhow::Result<u32> {    let number = get_number(num_err)?;    let price = get_price(price_err)?;    Ok(number * price)}fn get_number(err:bool)-> anyhow::Result<u32>{    if err {        Ok(100)    } else {        anyhow::bail!("Failed to get price")    }}fn get_price(err:bool) ->  anyhow::Result<u32> {    if !err {        Ok(100)    } else {        anyhow::bail!("Failed to get price")    }}

除了anyhow,還有很多其他類庫 如 thiserror, derive-error可以用於異常處理

8
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 程式設計師必須清楚的專業名詞,你知道幾個?