Rust這款語言並不強調Object-Oriented特性,也沒有既定的錯誤處理機制,所以自定義錯誤的方法也有别於其他慣用語言,這𥚃介紹使用enum來實作。

對哦,沒看錯,是用 enumeration

讓我們先看看如果模仿其他OO語言class extends Error的方式用structimpl Error是如何:

用struct實作標準Error Trait

Rust標準庫裡提供了 std::error::Error ,官方文檔裡可以找到例子教導我們如何在自己的struct上實作這個trait。

學習Rust時起初可能會這樣定義一個錯誤類型:

>> Try Online <<

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError;

impl Error for MyError {}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "My Error")
    }
}

fn main() {
    let err = MyError;
    println!("{:?}", err);
}

上面例子好像沒有太大問題,而當你的函數需要按情況返回不同的自定義錯誤時,大概會這樣做:

>> Try Online <<

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
use std::error::Error;
use std::fmt;
use rand::Rng;

/* Define PlaceNameError */
#[derive(Debug)]
struct PlaceNameError(String);

impl Error for PlaceNameError {}

impl fmt::Display for PlaceNameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Place of {} Not Found", &self.0)
    }
}
/* End of PlaceNameError */

/* Define GeoError */
#[derive(Debug)]
struct GeoError(f64, f64);

impl Error for GeoError {}

impl fmt::Display for GeoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Point({}, {}) Not Found", &self.0, &self.1)
    }
}
/* End of GeoError */

//function that results in either PlaceNameError or GeoError
fn do_something() -> Result<(), Box<dyn Error>> {
    let random_number: isize = rand::thread_rng().gen();
    if random_number % 2 == 0 {
        Err(Box::new(PlaceNameError("Mars".to_string())))
    }
    else {
        Err(Box::new(GeoError(2.0, 3.0)))
    }

}

fn main() {
    match do_something() {
        Ok(_) => println!("Nothing happen"),
        Err(err) => println!("{}", err),
    }
}

事情開始變得麻煩:

首先,為每個自定義錯誤類型編寫的代碼量很多;

因為函数會返回不同錯誤,返回值需要聲明為dyn Error,而又因為trait無法得知實際值的size,所以要用 std::boxed::Box 封装起來;

然後,你獲得一個Box<dyn Error>類型的錯誤,但它實際上到底是屬於哪個錯誤?Rust没有像instanceof這樣的關鍵字可以作出區分,所以"按照不同錯誤類型去作不同的處理"這件事變得很困難;

再者,錯誤值可能需要𢹂带一些額外信息,例如字串、地理位置、或者另一個struct等等,那應該要如何提取呢?因為難以區分類型,信息的提取也變得非常困難。

最後,當我們的代碼引用第三方庫時,你確定它返回的錯誤類型有實作標準庫的Error trait嗎?如果没有的話,我們便可能需要在業務邏輯中每次都主動把它封装成自定義錯誤類型。

enum實作標準Error Trait

為每一個錯誤類型定義struct和實作Error trait實在是廢時失事,来看看如果使用enum來寫會是甚麼樣子:

>> Try Online <<

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use std::error::Error;
use std::fmt;
use rand::Rng;

#[derive(Debug)]
enum MyError {
    PlaceNameError(String),
    GeoError(f64, f64),
}

impl Error for MyError {}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            MyError::PlaceNameError(ref name) => write!(f, "Place of {} Not Found", name),
            MyError::GeoError(ref x, ref y) => write!(f, "Point({}, {}) Not Found", x, y),
        }
    }
}

//function that results in either PlaceNameError or GeoError
fn do_something() -> Result<(), MyError> {
    let random_number: isize = rand::thread_rng().gen();
    if random_number % 2 == 0 {
        Err(MyError::PlaceNameError("Mars".to_string()))
    }
    else {
        Err(MyError::GeoError(2.0, 3.0))
    }
}

fn main() {
    match do_something() {
        Ok(_) => println!("Nothing happen"),
        Err(MyError::PlaceNameError(name)) => println!("{} is not a place name", name),
        Err(MyError::GeoError(x, y)) => println!("({}, {}) is not a correct location", x, y),
    }
}

這個enum侬然實作了Errorfmt::Display trait,能當作Error實例處理;

由於enum的size能夠在編澤時得知,所以不需要使用Box<T>封裝;

enum的Variants可以包含額外的資料,這是有别於其他主流語言的特性;

可利用match來配對實際的Variants,也就是説可以以此區分出各種類型,以及提取當中𢹂带的信息;

封装第三方錯誤類型

對於第三方庫的錯誤類型,無論是甚麽資料結構,我們都可以為其實作From<T>,以方便重新封装成自定義的Variant:

(由於字數問题所以省略了impl Errorimpl fmt::Display) >> Try Online <<

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use std::str::{from_utf8, Utf8Error};
use base64::{DecodeError};

#[derive(Debug)]
enum MyError {
    DecodeBase64Error(DecodeError),
    DecodeUtf8Error(Utf8Error),
}

impl From<DecodeError> for MyError {
    fn from(error: DecodeError) -> MyError {
        MyError::DecodeBase64Error(error)
    }
}

impl From<Utf8Error> for MyError {
    fn from(error: Utf8Error) -> MyError {
        MyError::DecodeUtf8Error(error)
    }
}

fn decode(data: &str) -> Result<String, MyError> {
    let bytes: &[u8] = &base64::decode(data)?[..];

    let string = from_utf8(bytes)?;

    Ok(string.to_owned())
}

fn main() {
    let samples = [
        "aGVsbG8gd29ybGQ=",
        "aGVsbG8gd29ybG",
        "p0GmbqFBpUCsyQ==",
    ];

    for sample in &samples {
        match decode(sample) {
            Ok(bytes) => println!("Decoded result: {:?}", bytes),
            Err(err) => println!("{:?}", err),
        }
    }
}

base64::decode無法正確解碼而返回Err(base64::DecodeError)時, '`?`'操作符 會讓do_something()返回這個Err(base64::DecodeError),但由於函數定義返回值是Result<String, MyError>,Rust會在編譯時尋找你定義的impl base64::DecodeError for MyError,然後以隠式轉換的方式應用上去,換句話説就是實現了自動封装,你不需要顯式地呼叫任何函數進行轉換。

在並發模型中使用failure::Fail

如果有在使用async function,想必你的錯誤類型需要同時實作Sync + Send,那麼 `failure::Fail` 可以幫助你大幅減少代碼量。它定義是:

1
2
3
pub trait Fail: Display + Debug + Send + Sync + 'static {
    //......
}

而你就只需要在enumderive(Fail)

1
2
3
4
5
#[derive(Fail, Debug)]
pub enum MyError {
    EncodeError,
    DecodeError,
}

然後你的錯誤類型便滿足並發要求了。