【譯】Rust 中的錯(cuò)誤處理

引言

本文內(nèi)容主要翻譯自 Andrew Gallant 的文章 Error Handling in Rust吼驶。

如同大多數(shù)的編程語(yǔ)言澜共,Rust 中也需要通過(guò)特定的方式處理錯(cuò)誤命雀。眾所周知,目前常見(jiàn)的錯(cuò)誤處理方式主要分為兩種:

  1. 異常機(jī)制(C#/Java/Python 等)珍昨;
  2. 返回錯(cuò)誤(Go/Rust 等)木蹬。

本文將會(huì)綜合全面地介紹 Rust 中的錯(cuò)誤處理,通過(guò)循序漸進(jìn)地引導(dǎo)亲怠,帶領(lǐng)初學(xué)者扎實(shí)地掌握這塊知識(shí)所计。如果我們不熟悉標(biāo)準(zhǔn)庫(kù)的話,我們可能會(huì)使用較為愚蠢的方式處理錯(cuò)誤团秽,這種會(huì)比較繁瑣主胧,產(chǎn)生很多樣板代碼。所以本文會(huì)演示如何借助標(biāo)準(zhǔn)庫(kù)讓錯(cuò)誤處理更加優(yōu)雅和簡(jiǎn)潔习勤。

Rust 錯(cuò)誤處理

說(shuō)明

本文的代碼放在作者的 博客倉(cāng)庫(kù)踪栋。
Rust Book, Error Handling 中也有關(guān)于錯(cuò)誤處理的部分,可以參照閱讀图毕。

本文篇幅巨長(zhǎng)夷都,主要是因?yàn)閷懥撕芏嚓P(guān)于 Sum Types 和組合子(Combinators)的內(nèi)容作為開(kāi)頭,逐步講解 Rust 錯(cuò)誤處理方式的改進(jìn)予颤。因此囤官,如果你認(rèn)為沒(méi)有必要的話,也可以略過(guò)蛤虐。以下是作者提供的簡(jiǎn)要指南:

  1. 如果你是 Rust 新手党饮,對(duì)于系統(tǒng)編程和富類型系統(tǒng)(expressive type systems)不太熟悉的話,推薦你從頭開(kāi)始閱讀(如果是完全沒(méi)有了解過(guò) Rust 的童鞋驳庭,推薦先閱讀下 Rust Book刑顺;
  2. 如果你從來(lái)沒(méi)有了解過(guò) Rust,但是對(duì)于函數(shù)式語(yǔ)言很熟悉(看到「代數(shù)數(shù)據(jù)類型(algebaric data types)」和「組合子(combinators)」不會(huì)讓你感到陌生)饲常,那也許可以直接跳過(guò)基礎(chǔ)部分蹲堂,從「多錯(cuò)誤類型」部分開(kāi)始閱讀,然后閱讀「標(biāo)準(zhǔn)庫(kù)中的 error traits」部分;
  3. 如果你已經(jīng)擁有 Rust 編程經(jīng)驗(yàn)贝淤,并且只想了解下錯(cuò)誤處理的方式最住,那么可以直接跳到最后稀并,看看作者給出的案例研究(當(dāng)然聪蘸,還有譯者給出的實(shí)際案例)。

運(yùn)行代碼

如果想要直接運(yùn)行樣例代碼值漫,可以參考下面的操作:

$ git clone git://github.com/BurntSushi/blog
$ cd blog/code/rust-error-handling
$ cargo run --bin NAME-OF-CODE-SAMPLE [ args ... ]

TL;TR

文章太長(zhǎng),如果沒(méi)耐心閱讀的話眶蕉,我們可以先來(lái)看看關(guān)于 Rust 錯(cuò)誤處理的一些總結(jié)勋乾。以下是作者提供的「經(jīng)驗(yàn)法則」,僅供參考枕磁,我們可以從中獲得一些啟發(fā)渡蜻。

  • 如果你正在編寫簡(jiǎn)短的示例代碼,并且可能會(huì)有不少錯(cuò)誤處理的負(fù)擔(dān)计济,這種場(chǎng)景下可以考慮使用 unwrap(比如 Result::unwrap, Option::unwrap 或者 Option::expect 等)茸苇。閱讀你代碼的人應(yīng)該知道如何以優(yōu)雅的姿勢(shì)處理錯(cuò)誤(如果 TA 不會(huì),把這篇文章甩給 TA 看)沦寂;
  • 如果你正在寫一個(gè)類似臨時(shí)腳本一樣的程序(quick-n-dirty program)学密,直接用 unwrap 也不用覺(jué)得羞愧。不過(guò)需要注意的是:如果別人接手你這的代碼传藏,那可能是要不爽的哦(畢竟沒(méi)有錯(cuò)誤處理不夠優(yōu)雅)腻暮;
  • 對(duì)于??的場(chǎng)景,如果你覺(jué)得直接用 unwrap 不太好(畢竟出錯(cuò)會(huì)直接 panic 掉)毯侦,那么可以考慮使用 Box<dyn Error> 或者 anyhow::Error 類型作為函數(shù)的錯(cuò)誤返回類型哭靖。如果使用了 anyhow crate,當(dāng)使用 nightly 版本的 Rust 時(shí)侈离,錯(cuò)誤會(huì)自動(dòng)擁有關(guān)聯(lián)的 backtrace试幽;
  • 要是上面的方法還不行,那么可以考慮自定義錯(cuò)誤類型卦碾,并且實(shí)現(xiàn) FromError trait铺坞,從而可以順利地使用 ? 操作符,讓錯(cuò)誤處理更加優(yōu)雅(要是你連 FromError trait 也不愿意動(dòng)手實(shí)現(xiàn)的話蔗坯,可以使用 thiserror 來(lái)自動(dòng)生成)康震;
  • 如果你正在編寫一個(gè)庫(kù),并且可能會(huì)產(chǎn)生錯(cuò)誤宾濒,推薦你自定義錯(cuò)誤類型腿短,并且實(shí)現(xiàn) std::error::Error trait,并且實(shí)現(xiàn)合適的 From trait绘梦,從而方便庫(kù)的調(diào)用者編寫代碼(因?yàn)榛?Rust 的相干性原則(coherence rules)橘忱,調(diào)用者不能為庫(kù)中定義的錯(cuò)誤類型實(shí)現(xiàn) From,因此這是庫(kù)作者的職責(zé))卸奉;
  • 學(xué)會(huì)使用 OptionResult 中定義的組合子钝诚,有時(shí)候只是使用它們可能比較索然無(wú)味,但是可以通過(guò)合理地組合使用 ? 操作符合組合子來(lái)改善代碼榄棵。and_then, mapunwrap_or 是作者比較喜歡的幾個(gè)組合子凝颇。

總結(jié)一下流程如下:


錯(cuò)誤定義流程

基礎(chǔ)知識(shí)

錯(cuò)誤處理可以看成是利用 分支判斷(case analysis) 邏輯來(lái)指示一次計(jì)算成功與否潘拱。優(yōu)雅的錯(cuò)誤處理方式,關(guān)鍵就是要考慮減少顯式編寫分支判斷邏輯的代碼拧略,同時(shí)還能保持代碼的可組合性(就是讓調(diào)用方有錯(cuò)誤處理的決定權(quán)芦岂,調(diào)用方可以在約到錯(cuò)誤時(shí) panic 或者只是打印出錯(cuò)誤消息)。

保持代碼的可組合性非常重要垫蛆,否則我們可能在任何遇到不可預(yù)期的異常時(shí)出現(xiàn) panicpanic 會(huì)導(dǎo)致當(dāng)前的線程棧展開(kāi)禽最,多數(shù)情況下,還會(huì)導(dǎo)致整個(gè)進(jìn)程退出)袱饭。示例如下:

// file: panic-simple
//
// Guess a number between 1 and 10.
// If it matches the number I had in mind, return true. Else, return false.
fn guess(n: i32) -> bool {
    if n < 1 || n > 10 {
        panic!("Invalid number: {}", n);
    }
    n == 5
}

fn main() {
    guess(11);
}

如果嘗試運(yùn)行上述代碼川无,程序會(huì)直接崩潰,并且會(huì)吐出下面的消息:

thread '<main>' panicked at 'Invalid number: 11', src/bin/panic-simple.rs:5

下面這個(gè)例子虑乖,對(duì)于用戶輸入更加不可預(yù)知懦趋。它預(yù)期用戶輸入一個(gè)整數(shù)字符串,然后將其轉(zhuǎn)換成整數(shù)后再乘以 2疹味,打印出結(jié)果:

// file: unwrap-double
use std::env;

fn main() {
    let mut argv = env::args();
    let arg: String = argv.nth(1).unwrap(); // error 1
    let n: i32 = arg.parse().unwrap(); // error 2
    println!("{}", 2 * n);
}

// $ cargo run --bin unwrap-double 5
// 10

如果我們輸入的是 0 個(gè)參數(shù)(error 1)愕够,或者干脆輸入不可轉(zhuǎn)換成整數(shù)的字符串(error 2),則會(huì)導(dǎo)致程序 panic 掉佛猛。

Unwrap 詳解

unwrap-double 示例中惑芭,雖然沒(méi)有顯式地使用 panic,但是它在遇到錯(cuò)誤時(shí)依然會(huì) panic 掉继找。這是因?yàn)?unwrap 內(nèi)部調(diào)用了 panic遂跟。

unwrap 在 Rust 中的意思是這樣的:根據(jù)提供的計(jì)算結(jié)果,如果是錯(cuò)誤結(jié)果婴渡,則會(huì)直接調(diào)用 panic幻锁。也許你開(kāi)始好奇 unwrap 真正的實(shí)現(xiàn)了(實(shí)現(xiàn)很簡(jiǎn)單),不過(guò)在我們學(xué)習(xí)完 OptionResult 類型后自然就知道了边臼。這兩種類型都有關(guān)聯(lián)的 unwrap 方法定義哄尔。

Option 類型

Option 類型是標(biāo)準(zhǔn)庫(kù)定義的枚舉類型:

enum Option<T> {
    None,
    Some(T),
}

在 Rust 中,我們可以利用 Option 類型來(lái)表達(dá)存在性柠并。將這種類型加入到 Rust 的類型系統(tǒng)非常重要岭接,這樣編譯器會(huì)強(qiáng)制使用者處理存在性。下面來(lái)看個(gè)簡(jiǎn)單的示例:

// file: option-ex-string-find
//
// Searches `haystack` for the Unicode character `needle`. If one is found, the
// byte offset of the character is returned. Otherwise, `None` is returned.
fn find(haystack: &str, needle: char) -> Option<usize> {
    for (offset, c) in haystack.char_indices() {
        if c == needle {
            return Some(offset);
        }
    }
    None
}

溫馨提示:不要在你的代碼中使用上述代碼臼予,直接使用標(biāo)準(zhǔn)庫(kù)提供的 find 方法

我們注意到鸣戴,上述函數(shù)在找到匹配的字符后,不會(huì)直接返回 offset粘拾,而是返回 Some(offset)窄锅。SomeOption 類型的值構(gòu)造器,可以將它想象成這樣的函數(shù):fn<T>(value: T) -> Option<T>缰雇。相應(yīng)地入偷,None 也是一個(gè)值構(gòu)造器追驴,不過(guò)它沒(méi)有參數(shù),可以將它想象成這樣的函數(shù):fn<T>() -> Option<T>疏之。

Oops氯檐,看起來(lái)有點(diǎn)無(wú)聊哦,不過(guò)故事還沒(méi)嘮完体捏。我們來(lái)看看怎么使用 find 函數(shù),下面就利用它查找文件擴(kuò)展名:

fn main_find() {
    let file_name = "foobar.rs";
    match find(file_name, '.') {
        Some(i) => println!("File extension: {}", &file_name[i+1..]),
        None => println!("No file extension found."),
    }
}

上述代碼對(duì)于 find 返回的 Option<usize> 使用了「模式匹配」來(lái)執(zhí)行分支判斷糯崎。事實(shí)上几缭,分支判斷是獲得 Option<T> 內(nèi)部值的唯一方式。因此我們必須要處理好 Option<T>None 的情況沃呢。

啊喂年栓,等下,在前面提到的 unwrap 是怎么實(shí)現(xiàn)的呢薄霜?沒(méi)錯(cuò)某抓,它的內(nèi)部也使用了分支判斷!我們也可以自己定義這樣的方法:

enum Option<T> {
    None,
    Some(T),
}

impl<T> Option<T> {
    fn unwrap(self) -> T {
        match self {
            Option::Some(val) => val,
            Option::None =>
              panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

unwrap 為我們封裝了分支判斷邏輯惰瓜,方便我們使用否副。不過(guò)由于它內(nèi)部使用了 panic! 調(diào)用,這也意味著它不具備可組合性崎坊。

組合 Option<T>

在前面的 find 函數(shù)編寫中备禀,我們使用它來(lái)查找文件名的擴(kuò)展名。但是并非所有文件名都有 .奈揍,所以文件名可能并沒(méi)有擴(kuò)展部分曲尸。這種存在性正是通過(guò)使用 Option<T> 類型來(lái)表達(dá)的。也就是說(shuō)男翰,編譯器會(huì)強(qiáng)制我們處理擴(kuò)展名不存在的情況另患。我們?cè)谏厦嫣幚淼帽容^簡(jiǎn)單,就是打印出沒(méi)有擴(kuò)展名這樣的提示信息了蛾绎。

查找文件擴(kuò)展名是常見(jiàn)的操作昆箕,所以有必要將其封裝成另一個(gè)函數(shù):

// file: option-ex-string-find
//
// Returns the extension of the given file name, where the extension is defined
// as all characters succeeding the first `.`.
// If `file_name` has no `.`, then `None` is returned.
fn extension_explicit(file_name: &str) -> Option<&str> {
    match find(file_name, '.') {
        None => None,
        Some(i) => Some(&file_name[i+1..]),
    }
}

溫馨提示:不要使用上述代碼,使用標(biāo)準(zhǔn)庫(kù)中定義的 extension 方法代替

代碼很簡(jiǎn)單租冠,不過(guò)需要特別注意的是 find 會(huì)強(qiáng)制我們考慮存在性問(wèn)題为严。這樣一來(lái),編譯器就不允許我們「偶然」粗心大意忘記處理文件擴(kuò)展名不存在的情況肺稀。但從另一個(gè)方面來(lái)看第股,每次顯式低執(zhí)行分支判斷邏輯顯得被笨拙。

事實(shí)上话原,在 extension_explicity 中使用的分支判斷邏輯是一種常見(jiàn)的模式:map夕吻,它可以在 Option<T> 為 None 時(shí)返回 None诲锹,否則將值傳遞給指定的函數(shù)執(zhí)行并獲取返回結(jié)果。

Rust 中可以輕松定義一個(gè)組合子涉馅,從而將這種模式抽象出來(lái):

// file: option-map
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
    match option {
        None => None,
        Some(value) => Some(f(value)),
    }
}

當(dāng)然归园,在標(biāo)準(zhǔn)庫(kù)中,map 是定義在 Option<T> 上的方法稚矿。

有了上面的組合子庸诱,我們可以對(duì) extension_explicit 方法簡(jiǎn)單重構(gòu)下,從而移除分支判斷邏輯:

// file: option-ex-string-find
// Returns the extension of the given file name, where the extension is defined
// as all characters succeeding the first `.`.
// If `file_name` has no `.`, then `None` is returned.
fn extension(file_name: &str) -> Option<&str> {
    find(file_name, '.').map(|i| &file_name[i+1..])
}

另外一種常見(jiàn)的模式是在 Option 值為 None 時(shí)晤揣,返回一個(gè)默認(rèn)值桥爽。舉個(gè)栗子,你的應(yīng)用可能會(huì)假定文件的默認(rèn)擴(kuò)展名是 rs昧识,這樣即使 extension_explicit 返回了 None钠四,也可以給個(gè)默認(rèn)的擴(kuò)展名。

如你所期跪楞,這樣的分支判斷邏輯并非限定于文件擴(kuò)展名缀去,它可以用于任何 Option<T> 類型:

// file: option-unwrap-or
fn unwrap_or<T>(option: Option<T>, default: T) -> T {
    match option {
        None => default,
        Some(value) => value,
    }
}

使用起來(lái)非常簡(jiǎn)單:

// file: option-ext-string-find
fn main() {
    assert_eq!(extension("foobar.csv").unwrap_or("rs"), "csv");
    assert_eq!(extension("foobar").unwrap_or("rs"), "rs");
}

溫馨提示:unwrap_or 在標(biāo)準(zhǔn)庫(kù)中是定義在 Option<T> 上的方法,所以我們?cè)诖耸褂昧藰?biāo)準(zhǔn)庫(kù)的方法甸祭。當(dāng)然缕碎,別忘了看看 更通用的 unwrap_or_else 方法哦

還有一個(gè)值得特別注意的組合子 and_then,它可以讓我們輕松地組合多個(gè)不同的計(jì)算結(jié)果(都使用了 Option<T>)池户。例如阎曹,本節(jié)很多代碼都在講如何在指定的文件名中查找擴(kuò)展名。為此煞檩,你需要首先從文件路徑中提取出文件名处嫌。雖然大多數(shù)的文件路徑都有文件名,但并非所有的都是這種情況斟湃。比如:., .., 或者 /.熏迹。

那么,接下來(lái)我們要挑戰(zhàn)的任務(wù)是從給定的文件路徑中找到文件擴(kuò)展名凝赛。先來(lái)看看顯式判斷的寫法:

// file: option-ex-string-find
fn file_path_ext_explicit(file_path: &str) -> Option<&str> {
    match file_name(file_path) {
        None => None,
        Some(name) => match extension(name) {
            None => None,
            Some(ext) => Some(ext),
        }
    }
}

fn file_name(file_path: &str) -> Option<&str> {
  // implementation elided
  unimplemented!()
}

看起來(lái)貌似可以使用 map 組合子來(lái)減少分支判斷代碼注暗,不過(guò)它的類型并不適合。具體來(lái)說(shuō)墓猎,map 接收的函數(shù)是專門處理 Option<T> 的內(nèi)部值捆昏。不過(guò)此處,我們希望允許調(diào)用者通過(guò)自定義函數(shù)返回另外一個(gè) Option毙沾,以下是通用的實(shí)現(xiàn)示例:

// file: option-and-then
fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
        where F: FnOnce(T) -> Option<A> {
    match option {
        None => None,
        Some(value) => f(value),
    }
}

現(xiàn)在骗卜,我們可以重構(gòu) file_path_ext 函數(shù)了:

fn file_path_ext(file_path: &str) -> Option<&str> {
    file_name(file_path).and_then(extension)
}

Option 類型還有很多其它組合子,所以推薦去熟悉下這些組合子的使用方式,它們通晨懿郑可以幫助減少分支判斷代碼举户。熟悉了這些組合子后也有助于對(duì) Result 定義的類似方法增進(jìn)了解遍烦,它們有很多類似的方法俭嘁。

使用 Option 的組合子能夠讓代碼更加優(yōu)雅,并且減少了很多顯式的分支判斷邏輯服猪。同時(shí)供填,也給予了調(diào)用方更多的可組合性,讓調(diào)用方能夠按照存在性自由處理罢猪。而 unwrap 之類的方法則不具備這種優(yōu)勢(shì)近她,是因?yàn)樗鼤?huì)在 Option<T>None 時(shí)直接 panic 掉。

Result 類型

Result 類型也是在[標(biāo)準(zhǔn)庫(kù)](defined in the standard library)中定義的坡脐。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result 類型可以看作 Option 類型的加強(qiáng)版,除了可以像 Option 那樣表達(dá)存在性房揭,還可以表達(dá)可能的錯(cuò)誤是什么备闲。通常來(lái)說(shuō),我們可以通過(guò) error 表明一些計(jì)算失敗的原因捅暴。下面的類型別名和實(shí)際的 Option<T> 在語(yǔ)義上是等價(jià)的:

type Option<T> = Result<T, ()>;

以上是讓 Result 的第二個(gè)類型參數(shù)保持為 ()(稱為 unit 或者空元組 empty tuple)恬砂。() 類型只對(duì)應(yīng)一個(gè)值,即 ()(你沒(méi)??錯(cuò)蓬痒,類型和值都使用了同一個(gè)術(shù)語(yǔ))泻骤。

Result 專門用來(lái)表示計(jì)算的兩種可能的結(jié)果。按照慣例梧奢,期望的結(jié)果當(dāng)然是 Ok狱掂,而另外一種非預(yù)期的結(jié)果自然是 Err

正如 Option亲轨,Result 類型也有 unwrap 方法實(shí)現(xiàn)趋惨。我們也可以自定義一個(gè)如下:

// file : result-def
impl<T, E: ::std::fmt::Debug> Result<T, E> {
    fn unwrap(self) -> T {
        match self {
            Result::Ok(val) => val,
            Result::Err(err) =>
              panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
        }
    }
}

這個(gè)基本和 Option::unwrap 的定義是一樣的,不過(guò)它在 panic! 時(shí)會(huì)帶上錯(cuò)誤值惦蚊。這樣調(diào)試起來(lái)就更簡(jiǎn)單些了器虾,不過(guò)這也要求我們給 E 參數(shù)加上 Debug trait 限定。由于絕大多數(shù)的類型都應(yīng)該滿足 Debug 限定蹦锋,所以這樣做在實(shí)踐中也沒(méi)什么問(wèn)題兆沙。

下面,來(lái)看個(gè)例子莉掂。

解析整數(shù)

Rust 標(biāo)準(zhǔn)庫(kù)讓字符串轉(zhuǎn)整數(shù)非常容易:

// file: result-num-unwrap
fn double_number(number_str: &str) -> i32 {
    2 * number_str.parse::<i32>().unwrap()
}

fn main() {
    let n: i32 = double_number("10");
    assert_eq!(n, 20);
}

此處葛圃,我們需要對(duì)使用 unwrap 的地方保持懷疑的態(tài)度。例如,當(dāng)輸入的字符串不能轉(zhuǎn)換時(shí)装悲,就會(huì)發(fā)生 panic:

thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', /home/rustbuild/src/rust-buildbot/slave/beta-dist-rustc-linux/build/src/libcore/result.rs:729

這看起來(lái)很不優(yōu)雅昏鹃,如果 panic 發(fā)生在我們使用的庫(kù)中,自然會(huì)更加煩惱诀诊。所以洞渤,我們應(yīng)當(dāng)嘗試在函數(shù)返回錯(cuò)誤,讓調(diào)用方自由決定做什么属瓣。這就意味著要修改 double_number 的返回類型载迄。但是要改成什么好呢?這就需要我們來(lái)看看標(biāo)準(zhǔn)庫(kù)中定義的 parse 簽名了:

impl str {
    fn parse<F: FromStr>(&self) -> Result<F, F::Err>;
}

Emm抡蛙,至少我們知道需要使用 Result 類型了护昧。當(dāng)然,也可能返回一個(gè) Option 類型粗截。畢竟惋耙,字符串要么可以轉(zhuǎn)成數(shù)字,要么轉(zhuǎn)換失敗熊昌,不是嗎绽榛?看起來(lái)挺合理,不過(guò)內(nèi)部實(shí)現(xiàn)需要區(qū)分不能轉(zhuǎn)換字符串為整數(shù)的原因婿屹。因此灭美,使用 Result 會(huì)更加合理些,因?yàn)檫@樣我們可以提供更多的錯(cuò)誤信息昂利,而不僅僅是「存在與否」届腐。

接下來(lái)看看,我們?cè)趺粗付ǚ祷仡愋湍胤浼椋可厦娴?parse 方法是能夠處理在標(biāo)準(zhǔn)庫(kù)中定義的所有數(shù)據(jù)類型的泛型版本犁苏。我們當(dāng)然也可以讓我們的函數(shù)變成泛型版本,不過(guò)目前為了簡(jiǎn)單起見(jiàn)扩所,只關(guān)心 i32 類型傀顾。

我們需要看看 FromStr 的實(shí)現(xiàn),關(guān)注下它的關(guān)聯(lián)類型Err碌奉。在我們這里短曾,關(guān)聯(lián)錯(cuò)誤類型為 std::num::ParseIntError。最終赐劣,我們可以將函數(shù)重構(gòu)如下:

// file: result-num-to-unwrap
use std::num::ParseIntError;

fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
    match number_str.parse::<i32>() {
        Ok(n) => Ok(2 * n),
        Err(err) => Err(err),
    }
}

fn main() {
    match double_number("10") {
        Ok(n) => assert_eq!(n, 20),
        Err(err) => println!("Error: {:?}", err),
    }
}

這樣看起來(lái)就好多了嫉拐,但是我們還是寫了很多代碼,尤其是分支判斷邏輯又出現(xiàn)了魁兼。那該怎么辦呢婉徘?Result 也定義了很多關(guān)聯(lián)的組合子,所以此處我們可以使用 map

// file: result-num-no-unwrap-map
use std::num::ParseIntError;

fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
    number_str.parse::<i32>().map(|n| 2 * n)
}

fn main() {
    match double_number("10") {
        Ok(n) => assert_eq!(n, 20),
        Err(err) => println!("Error: {:?}", err),
    }
}

Option 中定義的很多組合子,類似 unwrap_or, and_thenResult 類型中都可以使用盖呼。此外儒鹿,Result 還有第二個(gè) Err 類型,所以還有一些專門關(guān)聯(lián)錯(cuò)誤類型的組合子几晤,例如 map_error_else 等约炎。

Result 類型別名

看標(biāo)準(zhǔn)庫(kù)中,經(jīng)常會(huì)發(fā)現(xiàn)類似 Result<i32> 這樣的類型蟹瘾。但是圾浅,我們定義的 Result 類型明明有兩個(gè)類型參數(shù)呢!那是怎么做到只需要指定一個(gè)類型的憾朴?關(guān)鍵點(diǎn)就在于可以定義一個(gè) Result 類型別名狸捕,這樣可以把某個(gè)具體的類型固定下來(lái)。通常众雷,被固定的類型是錯(cuò)誤類型灸拍。接下來(lái),我們可以為上面定義的 Result 類型做個(gè)別名砾省,示例如下:

// file: result-num-no-unwrap-map-alias
use std::num::ParseIntError;
use std::result;

type Result<T> = result::Result<T, ParseIntError>;

fn double_number(number_str: &str) -> Result<i32> {
    unimplemented!();
}

為什么需要這么做呢鸡岗?假如我們有很多函數(shù)都要返回 ParseIntError 類型,通過(guò)類型別名就可以省心很多纯蛾,不用到處寫 ParseIntError 了纤房。

在標(biāo)準(zhǔn)庫(kù)最典型的應(yīng)用就是 io::Result 了纵隔,通常的用法是 io::Result<T>翻诉。

小插曲:unwrap 并非邪惡

雖然上面花了很多篇幅在講怎么避免使用 unwrap 或者可能導(dǎo)致 panic 的寫法,通常情況下捌刮,我們是應(yīng)該這么做碰煌。然而,有時(shí)候使用 unwrap 依然是明智的绅作。下面是一些可以考慮直接使用 unwrap 的場(chǎng)景:

  • 在示例代碼芦圾、臨時(shí)腳本(qunk-n-dirty code)中。有時(shí)候我們?cè)趯懙呐R時(shí)腳本程序俄认,對(duì)于錯(cuò)誤處理并不需要嚴(yán)肅處理个少,所以即便使用 unwrap 也沒(méi)什么毛病眯杏;
  • 想要通過(guò) panic 表明程序中存在 bug夜焦。當(dāng)你需要在代碼中阻止某些行為發(fā)生時(shí)(比如從空的 stack 中彈出元素),可以考慮使用 panic岂贩,這樣可以暴露出程序中的 bug茫经。

可能還有很多其它的場(chǎng)景,不在此一一列舉了。另外卸伞,當(dāng)我們使用 Option 時(shí)抹镊,通常使用它的 expect 方法會(huì)更好,這樣可以打印出自定義的有意義的錯(cuò)誤消息荤傲,也不至于在 panic 的時(shí)候手足無(wú)措垮耳。

歸根結(jié)底,作者的觀點(diǎn)是:保持良好的判斷力弃酌。對(duì)于任何事情都需要權(quán)衡氨菇,我們需要根據(jù)場(chǎng)景來(lái)判斷使用什么方法。

處理多種錯(cuò)誤類型

到目前為止妓湘,我們學(xué)習(xí)了很多關(guān)于使用 Option<T>Result<T, SomeError> 處理錯(cuò)誤的方式查蓉。但是,如果 OptionResult 同時(shí)出現(xiàn)呢榜贴?又或者我們同時(shí)遇到 Result<T, Error1>Result<T, Error2>豌研?接下來(lái)的挑戰(zhàn)就是處理多種組合的錯(cuò)誤類型,這也是本文的核心內(nèi)容唬党。

組合 OptionResult

至此鹃共,我們已經(jīng)學(xué)習(xí)了很多關(guān)于 Option h 和 Result 的組合子,并且可以使用這些組合子將不同的計(jì)算結(jié)果組合起來(lái)返回驶拱,不需要顯式地編寫分支判斷邏輯霜浴。

然而,現(xiàn)實(shí)代碼并非如此干凈蓝纲。有時(shí)阴孟,我們會(huì)有 OptionResult 類型的混合,這時(shí)候我們又要退化到顯式地編寫分支判斷邏輯了嗎税迷?或者有沒(méi)有可能繼續(xù)使用組合子呢永丝?

現(xiàn)在,讓我們復(fù)習(xí)下前面提到的例子:

use std::env;

fn main() {
    let mut argv = env::args();
    let arg: String = argv.nth(1).unwrap(); // error 1
    let n: i32 = arg.parse().unwrap(); // error 2
    println!("{}", 2 * n);
}

// $ cargo run --bin unwrap-double 5
// 10

鑒于我們已經(jīng)學(xué)會(huì)使用 Option, Result 以及相關(guān)的組合子了箭养,那我們可以嘗試重構(gòu)下上述代碼慕嚷,這樣可以合理地處理錯(cuò)誤,并且在發(fā)生錯(cuò)誤時(shí)不要輕易地 panic毕泌。

這里比較特別的是喝检,argv.nth(1) 返回的是 Option,而 arg.parse() 則返回 Result撼泛。顯然挠说,它們二者不可組合。這時(shí)坎弯,我們通撤牡樱可以將 Option 轉(zhuǎn)換成 Result译暂。在我們的例子中,缺少命令行參數(shù)將會(huì)無(wú)法正常工作撩炊。我們現(xiàn)在只需要使用 String 來(lái)描述錯(cuò)誤:

// file: error-double-string
use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

在這個(gè)例子中有幾個(gè)新的知識(shí)點(diǎn)值得介紹外永。第一點(diǎn)是使用 Option::ok_or 組合子:它可以將 Option 轉(zhuǎn)換成 Result,此處需要我們指定錯(cuò)誤消息拧咳。下面來(lái)看看它的定義:

// file: option-ok-def
fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    match option {
        Some(v) => Ok(v),
        None => Err(err),
    }
}

這里使用的另一個(gè)組合子是 Result::map_err伯顶,它類似于 Result::map,不過(guò)它將傳入的函數(shù)應(yīng)用在 error 部分骆膝。

這里之所以使用 map_err祭衩,是因?yàn)橛斜匾屽e(cuò)誤類型保持不變(因?yàn)槲覀冞€會(huì)使用 and_then)。因?yàn)槲覀冃枰獙?Option<String>(來(lái)自 argv.nth(1))轉(zhuǎn)換成 Result<String, String>阅签,所以也要將來(lái)自 arg.parse()ParseIntError 轉(zhuǎn)換成 String掐暮。

組合子的限制

IO 和解析命令行輸入是很常見(jiàn)的操作,所以我們將會(huì)繼續(xù)講一些和 IO 有關(guān)的例子來(lái)展開(kāi)錯(cuò)誤處理政钟。

先從簡(jiǎn)單的例子開(kāi)始吧路克,假設(shè)我們需要讀取一個(gè)文件,將其中每一行轉(zhuǎn)成數(shù)字形式养交,然后再乘以 2 后輸出精算。

雖然之前強(qiáng)調(diào)不要隨意使用 unwrap,但是我們可以在最開(kāi)始時(shí)候臨時(shí)使用它碎连。這樣我們可以集中精力解決問(wèn)題灰羽,而非處理錯(cuò)誤朦拖。先簡(jiǎn)單編寫出來(lái)惩系,后面在重構(gòu)它,完善錯(cuò)誤處理部分飞主。

// file: io-basic-unwrap
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> i32 {
    let mut file = File::open(file_path).unwrap(); // error 1
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap(); // error 2
    let n: i32 = contents.trim().parse().unwrap(); // error 3
    2 * n
}

fn main() {
    let doubled = file_double("foobar");
    println!("{}", doubled);
}

溫馨提示:這里使用了和 std::fs::File::open 相同的類型綁定 AsRef<Path>座每,這樣可以非常優(yōu)雅地接受任何類型的字符串作為路徑前鹅。

這里有三種可能的錯(cuò)誤會(huì)發(fā)生:

  1. 打開(kāi)文件摘悴;
  2. 從文件中讀取數(shù)據(jù)峭梳;
  3. 將文本數(shù)據(jù)轉(zhuǎn)換成數(shù)字。

前兩個(gè)錯(cuò)誤可以使用 std::io::Error 類型(這個(gè)可以從 std::fs::File::openstd::io::Read::read_to_string 返回錯(cuò)誤可以得知)表示蹂喻。第三個(gè)錯(cuò)誤則可以用 std::num::ParseIntError 類型葱椭。需要注意的是,io::Error 類型在標(biāo)準(zhǔn)庫(kù)中使用很廣泛口四。

接下來(lái)孵运,開(kāi)始重構(gòu) file_double 函數(shù)吧,這樣可以讓函數(shù)返回值可以和其它部分組合蔓彩,保證在發(fā)生上述錯(cuò)誤時(shí)治笨,不要直接 panic 掉驳概。這也就意味著當(dāng)任何操作失敗時(shí),函數(shù)需要返回具體的錯(cuò)誤】趵担現(xiàn)在的問(wèn)題是顺又,之前寫的 file_double 函數(shù)返回值是 i32,并不會(huì)包含任何錯(cuò)誤信息等孵。所以我們需要將返回值從 i32 轉(zhuǎn)換成別的類型稚照。

首先我們要做的決策是:我們是應(yīng)該使用 Option 還是 Result 呢?當(dāng)然俯萌,我們可以直接使用 Option果录,這樣報(bào)錯(cuò)時(shí),只需要返回 None 即可咐熙。這樣看起來(lái)不錯(cuò)弱恒,并且也比直接 panic 要好得多,但我們還可以做得更好棋恼。我們還可以使用 Result<i32, E> 來(lái)表達(dá)可能出現(xiàn)的錯(cuò)誤斤彼。但是這里的 E 又是什么呢?由于存在兩種類型的錯(cuò)誤可能發(fā)生蘸泻,我們需要將它們轉(zhuǎn)換成通用的類型悦施。一種可選項(xiàng)就是 String蛙吏,下面來(lái)看看修改后的效果:

// file: io-basic-error-string
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    File::open(file_path)
         .map_err(|err| err.to_string())
         .and_then(|mut file| {
              let mut contents = String::new();
              file.read_to_string(&mut contents)
                  .map_err(|err| err.to_string())
                  .map(|_| contents)
         })
         .and_then(|contents| {
              contents.trim().parse::<i32>()
                      .map_err(|err| err.to_string())
         })
         .map(|n| 2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

上面的代碼看起來(lái)還是有些凌亂屉栓。因?yàn)槲覀冃枰獙⒎祷仡愋徒y(tǒng)一為 Result<i32, String>资昧,這樣就不得不使用合適的組合子虎眨。上面是使用了 and_then, mapmap_err

掌握組合子的使用很重要镶摘,不過(guò)它也有些限制嗽桩,還可能把代碼弄得很凌亂。接下來(lái)看另外一種方式:提前返回凄敢。

提前返回

首先我們將上一個(gè)例子改造成提前返回的模式碌冶。由于我們難以在 file_double 中的閉包中直接返回,所以需要改成顯式分支判斷邏輯涝缝。

// file: io-basic-error-string-early-return
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = match File::open(file_path) {
        Ok(file) => file,
        Err(err) => return Err(err.to_string()),
    };
    let mut contents = String::new();
    if let Err(err) = file.read_to_string(&mut contents) {
        return Err(err.to_string());
    }
    let n: i32 = match contents.trim().parse() {
        Ok(n) => n,
        Err(err) => return Err(err.to_string()),
    };
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

關(guān)于是使用組合子好扑庞,還是上面的寫法好,仁者見(jiàn)仁俊卤。當(dāng)然嫩挤,上面的寫法明顯更加直觀易懂害幅,當(dāng)發(fā)生錯(cuò)誤后直接終止函數(shù)執(zhí)行消恍,返回錯(cuò)誤接即可。

看起來(lái)像是退步了一樣以现,先前我們一再?gòu)?qiáng)調(diào)優(yōu)雅錯(cuò)誤處理的關(guān)鍵就是減少顯式的分支判斷邏輯狠怨,顯然這里我們違背了之前的原則约啊。不過(guò),我們還有別的辦法來(lái)解決顯式分支判斷過(guò)多的問(wèn)題佣赖,下面來(lái)看看吧恰矩。

使用 try! 宏或者 ? 操作符

早期版本的 Rust 中(1.12 版本及以前),錯(cuò)誤處理的基石之一是 try! 宏憎蛤。try! 類似于組合子外傅,抽象了分支判斷邏輯,同時(shí)也抽象了控制流(control flow)俩檬。這樣就可以應(yīng)用提前返回模式了萎胰。

以下是一個(gè)簡(jiǎn)單版本的 try! 宏:

// file: try-def-simple
macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(err),
    });
}

我們來(lái)看看借助于 try! 改造后的效果:

// file: io-basic-error-try
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = try!(File::open(file_path).map_err(|e| e.to_string()));
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents).map_err(|e| e.to_string()));
    let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string()));
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

而在 Rust 1.13 之后,try! 就被 ? 操作符替代了棚辽。改寫后如下:

// file: io-basic-error-question
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = File::open(file_path).map_err(|e| e.to_string())?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).map_err(|e| e.to_string())?;
    let n = contents.trim().parse::<i32>().map_err(|e| e.to_string())?;
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

自定義錯(cuò)誤類型

在深入學(xué)習(xí)標(biāo)準(zhǔn)庫(kù) error trait 之前技竟,接下來(lái)我們要做的是將 Result<i32, String> 中的 String 替換成自定義 Error。因?yàn)橹苯邮褂?String 有如下兩個(gè)缺點(diǎn):

  1. 容易污染代碼屈藐,搞得到處都是錯(cuò)誤消息字符串榔组;
  2. 字符串會(huì)丟失信息(比如錯(cuò)誤類型,錯(cuò)誤來(lái)源等)联逻。

舉個(gè)例子搓扯,io::Error 嵌套了 io::ErrorKind,它是結(jié)構(gòu)化的數(shù)據(jù)包归,用來(lái)表示 IO 操作時(shí)具體的錯(cuò)誤擅编。這一點(diǎn)非常重要,因?yàn)槲覀兛赡苄枰槍?duì)不同的錯(cuò)誤做出不同的響應(yīng)箫踩。所以有了 io::ErrorKind 后爱态,調(diào)用方就可以基于具體的錯(cuò)誤類型進(jìn)行處理了,而這顯然要比從 String 中提取錯(cuò)誤原因更加嚴(yán)格境钟。

所以接下來(lái)锦担,我們要定義自己的錯(cuò)誤類型,盡可能避免將下層錯(cuò)誤給丟掉慨削,這樣如果調(diào)用方需要查看錯(cuò)誤詳情洞渔,也不至于無(wú)計(jì)可施。

我們可以通過(guò) enum 來(lái)定義包含多種錯(cuò)誤的類型缚态,示例如下:

// file: io-basic-error-custom
use std::io;
use std::num;

// We derive `Debug` because all types should probably derive `Debug`.
// This gives us a reasonable human readable description of `CliError` values.
#[derive(Debug)]
enum CliError {
    Io(io::Error),
    Parse(num::ParseIntError),
}

緊接著磁椒,我們只需要將之前例子中的 String error 替換成自定義的 CliError 即可:

// file: io-basic-error-custom
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> {
    let mut file = File::open(file_path).map_err(CliError::Io)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).map_err(CliError::Io)?;
    let n: i32 = contents.trim().parse().map_err(CliError::Parse)?;
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {:?}", err),
    }
}

標(biāo)準(zhǔn)庫(kù)中的錯(cuò)誤處理 trait

標(biāo)準(zhǔn)庫(kù)定義了兩種錯(cuò)誤處理必備的 trait:

Error trait

Error trait 的定義如下:

use std::fmt::{Debug, Display};

trait Error: Debug + Display {
  /// A short description of the error.
  fn description(&self) -> &str;

  /// The lower level cause of this error, if any.
  fn cause(&self) -> Option<&Error> { None }
}

這個(gè) trait 是非常通用的医增,也是所有表示錯(cuò)誤的類型都要實(shí)現(xiàn)的 trait慎皱。總的來(lái)說(shuō)叶骨,這個(gè) trait 可以讓我們做這么幾件事情:

  • 獲取錯(cuò)誤表示的 Debug 表示(需要實(shí)現(xiàn) std::fmt::Debug trait)茫多;
  • 獲取錯(cuò)誤的 Display 表示(需要實(shí)現(xiàn) std::fmt::Display trait);
  • 獲取錯(cuò)誤的簡(jiǎn)要描述(description 方法)忽刽;
  • 獲取錯(cuò)誤鏈(可以通過(guò) cause 方法獲得)天揖。

由于所有的錯(cuò)誤都實(shí)現(xiàn)了 Error trait,那我們就可以將錯(cuò)誤表示成 trait object 了(比如表示成 Box<dyn Error> 或者 &dyn Error)跪帝。

接下來(lái)宝剖,我們可以為 CliError 實(shí)現(xiàn) Error trait:

// file: error-impl
use std::io;
use std::num;
use std::error;
use std::fmt;

// We derive `Debug` because all types should probably derive `Debug`.
// This gives us a reasonable human readable description of `CliError` values.
#[derive(Debug)]
enum CliError {
    Io(io::Error),
    Parse(num::ParseIntError),
}

impl fmt::Display for CliError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            // Both underlying errors already impl `Display`, so we defer to
            // their implementations.
            CliError::Io(ref err) => write!(f, "IO error: {}", err),
            CliError::Parse(ref err) => write!(f, "Parse error: {}", err),
        }
    }
}

impl error::Error for CliError {
    fn description(&self) -> &str {
        // Both underlying errors already impl `Error`, so we defer to their
        // implementations.
        match *self {
            CliError::Io(ref err) => err.description(),
            // Normally we can just write `err.description()`, but the error
            // type has a concrete method called `description`, which conflicts
            // with the trait method. For now, we must explicitly call
            // `description` through the `Error` trait.
            CliError::Parse(ref err) => error::Error::description(err),
        }
    }

    fn cause(&self) -> Option<&dyn error::Error> {
        match *self {
            // N.B. Both of these implicitly cast `err` from their concrete
            // types (either `&io::Error` or `&num::ParseIntError`)
            // to a trait object `&Error`. This works because both error types
            // implement `Error`.
            CliError::Io(ref err) => Some(err),
            CliError::Parse(ref err) => Some(err),
        }
    }
}

From trait

先來(lái)看看 From trait 的定義吧:

trait From<T> {
    fn from(T) -> Self;
}

看起來(lái)超級(jí)簡(jiǎn)單吧,From 非常有用歉甚,它提供了一種通用的方式將一種類型轉(zhuǎn)換成另一種類型万细。可以來(lái)看看幾個(gè)簡(jiǎn)單的例子:

let string: String = From::from("foo");
let bytes: Vec<u8> = From::from("foo");
let cow: ::std::borrow::Cow<str> = From::from("foo");

好吧纸泄,看起來(lái)字符串之間的轉(zhuǎn)換使用 From 非常便捷赖钞。但是關(guān)于錯(cuò)誤的轉(zhuǎn)換呢?事實(shí)上關(guān)于錯(cuò)誤有這么一個(gè) From 轉(zhuǎn)換實(shí)現(xiàn):

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

這就一折任意實(shí)現(xiàn)了 Error trait 的類型聘裁,都可以轉(zhuǎn)換成 Box<dyn Error> trait object雪营。看起來(lái)并沒(méi)有多么讓人吃驚衡便,但是很實(shí)用献起。來(lái)看看下面的例子:

// file: from-example-errors
use std::error::Error;
use std::fs;
use std::io;
use std::num;

// We have to jump through some hoops to actually get error values.
let io_err: io::Error = io::Error::last_os_error();
let parse_err: num::ParseIntError = "not a number".parse::<i32>().unwrap_err();

// OK, here are the conversions.
let err1: Box<Error> = From::from(io_err);
let err2: Box<Error> = From::from(parse_err);

上面的 err1err2 的類型都是相同的類型,這里都是 trait object镣陕。對(duì)于編譯器而言谴餐,它們的底層類型已經(jīng)被移除了。上面實(shí)用 From::from 的模式非常重要呆抑,它為我們提供了一種可靠且一致的方式將錯(cuò)誤轉(zhuǎn)換成相同的類型岂嗓。

是時(shí)候復(fù)習(xí)下我們的老伙計(jì) try!? 操作符了。

try!? 內(nèi)部實(shí)現(xiàn)

try! 的內(nèi)部實(shí)現(xiàn)并非和前面提到那樣簡(jiǎn)單鹊碍,在內(nèi)部還執(zhí)行了額外的轉(zhuǎn)換操作:

macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(::std::convert::From::from(err)),
    });
}

? 的工作原理也是類似的厌殉,只是定義上有些許不同,看起來(lái)可能像下面這樣:

match ::std::ops::Try::into_result(x) {
    Ok(v) => v,
    Err(e) => return ::std::ops::Try::from_error(From::from(e)),
}

讓我們來(lái)回憶下之前用于讀取文件并轉(zhuǎn)換成整數(shù)的代碼:

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = File::open(file_path).map_err(|e| e.to_string())?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).map_err(|e| e.to_string())?;
    let n = contents.trim().parse::<i32>().map_err(|e| e.to_string())?;
    Ok(2 * n)
}

既然我們已經(jīng)知道 ? 實(shí)現(xiàn)中涉及了 From::from 調(diào)用侈咕,即錯(cuò)誤類型的自動(dòng)轉(zhuǎn)換公罕, 那么我們將函數(shù)的返回錯(cuò)誤使用 Box<dyn Error> 表示,從而將錯(cuò)誤轉(zhuǎn)換成 trait object:

// file: io-basic-error-try-from
use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<dyn Error>> {
    let mut file = File::open(file_path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let n = contents.trim().parse::<i32>()?;
    Ok(2 * n)
}

現(xiàn)在耀销,我們已經(jīng)越來(lái)越接近完美的錯(cuò)誤處理模式了楼眷。由于 ? 操作符的加持,為我們節(jié)省了很多代碼,同時(shí)提供了如下幾種能力:

  1. 分支判斷摩桶;
  2. 控制流桥状;
  3. 錯(cuò)誤類型轉(zhuǎn)換帽揪。

但是硝清,還有一個(gè)小瑕疵沒(méi)有解決。Box<Error> 是模糊的转晰,我們沒(méi)法得知原先的錯(cuò)誤類型芦拿,雖然相比于 String,我們可以通過(guò) descriptioncause 獲取到更詳細(xì)的錯(cuò)誤信息查邢。接下來(lái)蔗崎,我們需要解決這個(gè)小瑕疵。

組合自定義錯(cuò)誤類型

首先要為我們的 CliError 實(shí)現(xiàn) From 轉(zhuǎn)換扰藕,這樣在使用 ? 操作符時(shí)缓苛,就可以完成錯(cuò)誤類型的自動(dòng)轉(zhuǎn)換了。

impl From<io::Error> for CliError {
    fn from(err: io::Error) -> CliError {
        CliError::Io(err)
    }
}

impl From<num::ParseIntError> for CliError {
    fn from(err: num::ParseIntError) -> CliError {
        CliError::Parse(err)
    }
}

然后將 file_double 重構(gòu)如下:

// file: io-basic-error-custom-from
fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> {
    let mut file = File::open(file_path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let n: i32 = contents.trim().parse()?;
    Ok(2 * n)
}

如果我們想要讓 file_double 將字符串轉(zhuǎn)換成 float邓深,就需要添加一個(gè)新的錯(cuò)誤類型:

enum CliError {
    Io(io::Error),
    ParseInt(num::ParseIntError),
    ParseFloat(num::ParseFloatError),
}

然后實(shí)現(xiàn)錯(cuò)誤類型轉(zhuǎn)換 trait 即可:

impl From<num::ParseIntError> for CliError {
    fn from(err: num::ParseIntError) -> CliError {
        CliError::ParseInt(err)
    }
}

impl From<num::ParseFloatError> for CliError {
    fn from(err: num::ParseFloatError) -> CliError {
        CliError::ParseFloat(err)
    }
}

給庫(kù)作者的一些建議

如果我們需要在自己的庫(kù)中報(bào)告自定義的錯(cuò)誤未桥,那可能就需要自定義錯(cuò)誤類型。我們可以決定要不要暴露錯(cuò)誤實(shí)現(xiàn)(類似 ErrorKind)或者隱藏實(shí)現(xiàn)(類似 ParseIntError)芥备。不管怎樣冬耿,都要比直接用 String 表示錯(cuò)誤更加科學(xué)。

對(duì)于我們的自定義錯(cuò)誤萌壳,都應(yīng)當(dāng)實(shí)現(xiàn) Error trait亦镶,這樣也為庫(kù)的使用者提供了錯(cuò)誤組合的靈活性,同時(shí)也可以讓用戶有從錯(cuò)誤中獲得詳細(xì)信息的能力(需要實(shí)現(xiàn) fmt::Debugfmt::Display)袱瓮。

此外缤骨,我們也需要為自定義錯(cuò)誤類型提供 From 的實(shí)現(xiàn),這樣就可以組合更多詳細(xì)錯(cuò)誤了尺借。比如荷憋,csv::Error 就提供了來(lái)自 io::Errorbyteorder::Error 的轉(zhuǎn)換實(shí)現(xiàn)。

最后褐望,我們也可以根據(jù)情況定義 Result 類型別名勒庄。

案例研究:閱讀人口數(shù)據(jù)的小程序

接下來(lái),將構(gòu)建一個(gè)命令行程序瘫里,用于查詢指定地方的人口數(shù)據(jù)实蔽。目標(biāo)很簡(jiǎn)單:接收輸入的位置,輸出關(guān)聯(lián)的人口數(shù)據(jù)谨读【肿埃看起來(lái)挺簡(jiǎn)單,不過(guò)還是有很多可能出錯(cuò)的地方!

下面我們需要使用的數(shù)據(jù)來(lái)自 數(shù)據(jù)科學(xué)工具箱铐尚,可以從 這里 獲取世界人口數(shù)據(jù)拨脉,或者從 這里 獲取美帝人口數(shù)據(jù)。

它在 GitHub 上

代碼托管在 GitHub 上宣增,如果已經(jīng)安裝好 了 Rust 和 Cargo玫膀,可以直接克隆下來(lái)運(yùn)行:

git clone git://github.com/BurntSushi/rust-error-handling-case-study
cd rust-error-handling-case-study
cargo build --release
./target/release/city-pop --help

不過(guò)下面我們將一點(diǎn)點(diǎn)地編寫好這個(gè)程序~

配置

首先創(chuàng)建新的項(xiàng)目:cargo new --bin city-pop,然后確保 Cargo.toml 類似下面這樣:

[package]
name = "city-pop"
version = "0.1.0"
authors = ["Andrew Gallant <jamslam@gmail.com>"]

[[bin]]
name = "city-pop"

[dependencies]
csv = "0.*"
docopt = "0.*"
rustc-serialize = "0.*"

緊接著就可以編譯運(yùn)行了:

cargo build --release
./target/release/city-pop
#Outputs: Hello, world!

參數(shù)解析

我們使用的命令行解析工具是 Docopt爹脾,它可以從使用幫助中生成合適的命令行解析器帖旨。完成命令行解析后,就可以將應(yīng)用參數(shù)解碼到相應(yīng)的參數(shù)結(jié)構(gòu)體上了灵妨。接下來(lái)看看下面的示例:

extern crate docopt;
extern crate rustc_serialize;

static USAGE: &'static str = "
Usage: city-pop [options] <data-path> <city>
       city-pop --help

Options:
    -h, --help     Show this usage message.
";

struct Args {
    arg_data_path: String,
    arg_city: String,
}

fn main() {

}

好啦解阅,開(kāi)始擼代碼吧。依據(jù) Docopt 的文檔泌霍,我們可以使用 Docopt::new 新建一個(gè) parser货抄,然后使用 Docopt::decode 將命令參數(shù)解碼到一個(gè)結(jié)構(gòu)體中。這些函數(shù)都會(huì)返回 doct::Error朱转,我們先顯式地編寫分支判斷邏輯吧:

// These use statements were added below the `extern` statements.
// I'll elide them in the future. Don't worry! It's all on Github:
// https://github.com/BurntSushi/rust-error-handling-case-study
//use std::io::{self, Write};
//use std::process;
//use docopt::Docopt;

fn main() {
    let args: Args = match Docopt::new(USAGE) {
        Err(err) => {
            writeln!(&mut io::stderr(), "{}", err).unwrap();
            process::exit(1);
        }
        Ok(dopt) => match dopt.decode() {
            Err(err) => {
                writeln!(&mut io::stderr(), "{}", err).unwrap();
                process::exit(1);
            }
            Ok(args) => args,
        }
    };
}

看起來(lái)還是不夠簡(jiǎn)潔蟹地,一種可以改進(jìn)的方式是編寫一個(gè)宏,將消息打印到 stderr 后退出:

macro_rules! fatal {
    ($($tt:tt)*) => {{
        use std::io::Write;
        writeln!(&mut ::std::io::stderr(), $($tt)*).unwrap();
        ::std::process::exit(1)
    }}
}

此處使用 unwrap 是可以的肋拔,因?yàn)橐坏﹫?zhí)行失敗锈津,就意味著程序無(wú)法向 stderr 寫入了。

代碼看起來(lái)好多了凉蜂,不過(guò)還是有一處顯式分支判斷邏輯:

let args: Args = match Docopt::new(USAGE) {
    Err(err) => fatal!("{}", err),
    Ok(dopt) => match dopt.decode() {
        Err(err) => fatal!("{}", err),
        Ok(args) => args,
    }
};

謝天謝地琼梆,docopt::Error 類型定義了一個(gè) exit 方法,這樣我們就可以拋棄上面的 match 模式匹配窿吩,換用組合子編寫出更加緊湊的代碼了:

let args: Args = Docopt::new(USAGE)
                        .and_then(|d| d.decode())
                        .unwrap_or_else(|err| err.exit());

編寫業(yè)務(wù)邏輯

我們需要解析 csv 數(shù)據(jù)茎杂,并將符合條件的行打印出來(lái)即可。接下來(lái)看看怎么完成這個(gè)任務(wù):

// This struct represents the data in each row of the CSV file.
// Type based decoding absolves us of a lot of the nitty gritty error
// handling, like parsing strings as integers or floats.
struct Row {
    country: String,
    city: String,
    accent_city: String,
    region: String,

    // Not every row has data for the population, latitude or longitude!
    // So we express them as `Option` types, which admits the possibility of
    // absence. The CSV parser will fill in the correct value for us.
    population: Option<u64>,
    latitude: Option<f64>,
    longitude: Option<f64>,
}

fn main() {
    let args: Args = Docopt::new(USAGE)
                            .and_then(|d| d.decode())
                            .unwrap_or_else(|err| err.exit());

    let file = fs::File::open(args.arg_data_path).unwrap();
    let mut rdr = csv::Reader::from_reader(file);
    for row in rdr.decode::<Row>() {
        let row = row.unwrap();
        if row.city == args.arg_city {
            println!("{}, {}: {:?}",
                     row.city, row.country,
                     row.population.expect("population count"));
        }
    }
}

來(lái)來(lái)來(lái)纫雁,我們來(lái)梳理下有哪些錯(cuò)誤(看看 unwrap 調(diào)用的地方):

  1. fs::File::open 會(huì)返回 io::Error煌往;
  2. csv::Reader::decode 每次解碼一條記錄,該操作會(huì)產(chǎn)生 csv::Error(參見(jiàn) Iterator 實(shí)現(xiàn)中關(guān)聯(lián)的 Item 類型)轧邪;
  3. 如果 row.populationNone刽脖,那么調(diào)用 expect 會(huì)導(dǎo)致 panic。

那還有別的錯(cuò)誤嗎忌愚?萬(wàn)一城市不存在呢曲管?類似于 grep 的工具會(huì)在這種情況下返回錯(cuò)誤碼,所以我們也可能需要這樣做硕糊。

接下來(lái)院水,我們來(lái)看看兩種處理這些錯(cuò)誤的方式腊徙。先從 Box<dyn Error> 開(kāi)始吧~

使用 Box<dyn Error> 處理錯(cuò)誤

Box<dyn Error> 最大的好處是簡(jiǎn)單易用,我們不需要為錯(cuò)誤類型實(shí)現(xiàn)任何 From 轉(zhuǎn)換檬某。但缺點(diǎn)也在前面提過(guò)撬腾,它由于是一個(gè) trait object,會(huì)丟失底層的錯(cuò)誤類型恢恼。

接下來(lái)開(kāi)始我們的重構(gòu)吧民傻,首先需要把我們的業(yè)務(wù)邏輯抽到一個(gè)單獨(dú)的函數(shù)中處理,依然保留 unwrap 調(diào)用厅瞎,同時(shí)對(duì)于沒(méi)有人數(shù)的城市饰潜,我們把那行忽略即可初坠。

struct Row {
    // unchanged
}

struct PopulationCount {
    city: String,
    country: String,
    // This is no longer an `Option` because values of this type are only
    // constructed if they have a population count.
    count: u64,
}

fn search<P: AsRef<Path>>(file_path: P, city: &str) -> Vec<PopulationCount> {
    let mut found = vec![];
    let file = fs::File::open(file_path).unwrap();
    let mut rdr = csv::Reader::from_reader(file);
    for row in rdr.decode::<Row>() {
        let row = row.unwrap();
        match row.population {
            None => { } // skip it
            Some(count) => if row.city == city {
                found.push(PopulationCount {
                    city: row.city,
                    country: row.country,
                    count: count,
                });
            },
        }
    }
    found
}

fn main() {
    let args: Args = Docopt::new(USAGE)
                            .and_then(|d| d.decode())
                            .unwrap_or_else(|err| err.exit());

    for pop in search(&args.arg_data_path, &args.arg_city) {
        println!("{}, {}: {:?}", pop.city, pop.country, pop.count);
    }
}

雖然我們已經(jīng)移除了一處 expect(它是 unwrap 更優(yōu)雅的變種)和簸,但我們?nèi)匀恍枰幚硭阉鹘Y(jié)果的存在性問(wèn)題。

為了能夠用合適的方式處理錯(cuò)誤碟刺,需要對(duì)代碼做如下幾處修改:

fn search<P: AsRef<Path>>
         (file_path: P, city: &str)
         -> Result<Vec<PopulationCount>, Box<Error+Send+Sync>> {
    let mut found = vec![];
    let file = fs::File::open(file_path)?;
    let mut rdr = csv::Reader::from_reader(file);
    for row in rdr.decode::<Row>() {
        let row = row?;
        match row.population {
            None => { } // skip it
            Some(count) => if row.city == city {
                found.push(PopulationCount {
                    city: row.city,
                    country: row.country,
                    count: count,
                });
            },
        }
    }
    if found.is_empty() {
        Err(From::from("No matching cities with a population were found."))
    } else {
        Ok(found)
    }
}

這里我們使用 ? 替代了 unwrap 調(diào)用锁保,但這段代碼還有一個(gè)問(wèn)題:我們?cè)谶@里使用的是 Box<Error + Send + Sync> 而非 Box<Error>。之所以選擇這樣做半沽,是為了方便將字符串轉(zhuǎn)換成錯(cuò)誤類型爽柒。看到下面的 From 轉(zhuǎn)換實(shí)現(xiàn)就可以明白了:

/ We are making use of this impl in the code above, since we call `From::from`
// on a `&'static str`.
impl<'a, 'b> From<&'b str> for Box<Error + Send + Sync + 'a>

// But this is also useful when you need to allocate a new string for an
// error message, usually with `format!`.
impl From<String> for Box<Error + Send + Sync>

現(xiàn)在我們已經(jīng)知道怎么使用 Box<Error> 處理錯(cuò)誤了者填,接下來(lái)我們嘗試另外一種方式:自定義錯(cuò)誤類型浩村。

stdin 中讀取

為應(yīng)用添加從 stdin 讀取的支持非常簡(jiǎn)單,我們只需做出如下兩處調(diào)整即可:

  1. 調(diào)整程序可以接收一個(gè)城市參數(shù)占哟,而人口數(shù)據(jù)可以從 stdin 中讀刃氖;
  2. 修改 search 函數(shù)榨乎,這樣可以接收一個(gè)可選的路徑怎燥,當(dāng)為 None 時(shí),需要從 stdin 中讀取數(shù)據(jù)蜜暑。

我們先來(lái)調(diào)整下命令行結(jié)構(gòu)體和使用幫助:

static USAGE: &'static str = "
Usage: city-pop [options] [<data-path>] <city>
       city-pop --help

Options:
    -h, --help     Show this usage message.
";

struct Args {
    arg_data_path: Option<String>,
    arg_city: String,
}

search 的修改更加特別铐姚,csv crate 可以基于任何實(shí)現(xiàn)了 io::Read 的類型構(gòu)建 parser。但我們?nèi)绾尾拍茉趦煞N類型上使用相同的代碼呢肛捍?我們嘗試下使用 trait object:

fn search<P: AsRef<Path>>
         (file_path: &Option<P>, city: &str)
         -> Result<Vec<PopulationCount>, Box<Error+Send+Sync>> {
    let mut found = vec![];
    let input: Box<io::Read> = match *file_path {
        None => Box::new(io::stdin()),
        Some(ref file_path) => Box::new(fs::File::open(file_path)?),
    };
    let mut rdr = csv::Reader::from_reader(input);
    // The rest remains unchanged!
}

使用自定義錯(cuò)誤類型

根據(jù)前面的分析隐绵,有三種類型的錯(cuò)誤,所以我們的自定義錯(cuò)誤類型如下:

enum CliError {
    Io(io::Error),
    Csv(csv::Error),
    NotFound,
}

impl fmt::Display for CliError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            CliError::Io(ref err) => err.fmt(f),
            CliError::Csv(ref err) => err.fmt(f),
            CliError::NotFound => write!(f, "No matching cities with a \
                                             population were found."),
        }
    }
}

impl Error for CliError {
    fn description(&self) -> &str {
        match *self {
            CliError::Io(ref err) => err.description(),
            CliError::Csv(ref err) => err.description(),
            CliError::NotFound => "not found",
        }
    }
}

在我們正式使用 CliError 前拙毫,還需要實(shí)現(xiàn)一組 From依许,用于錯(cuò)誤類型轉(zhuǎn)換:

impl From<io::Error> for CliError {
    fn from(err: io::Error) -> CliError {
        CliError::Io(err)
    }
}

impl From<csv::Error> for CliError {
    fn from(err: csv::Error) -> CliError {
        CliError::Csv(err)
    }
}

接下來(lái)完成 search 函數(shù)的改造:

fn search<P: AsRef<Path>>
         (file_path: &Option<P>, city: &str)
         -> Result<Vec<PopulationCount>, CliError> {
    let mut found = vec![];
    let input: Box<io::Read> = match *file_path {
        None => Box::new(io::stdin()),
        Some(ref file_path) => Box::new(fs::File::open(file_path)?),
    };
    let mut rdr = csv::Reader::from_reader(input);
    for row in rdr.decode::<Row>() {
        let row = row?;
        match row.population {
            None => { } // skip it
            Some(count) => if row.city == city {
                found.push(PopulationCount {
                    city: row.city,
                    country: row.country,
                    count: count,
                });
            },
        }
    }
    if found.is_empty() {
        Err(CliError::NotFound)
    } else {
        Ok(found)
    }
}

獨(dú)創(chuàng)案例研究:新冠數(shù)據(jù)查詢

2020 年的新冠肺炎對(duì)人類社會(huì)來(lái)說(shuō),是一場(chǎng)突如其來(lái)的打擊恬偷。接下來(lái)我們寫一個(gè)簡(jiǎn)單的命令行程序悍手,用來(lái)根據(jù)指定的國(guó)家返回感染人數(shù)帘睦、死亡人數(shù)等數(shù)據(jù)√箍担可以在這個(gè)倉(cāng)庫(kù)查看完整源碼竣付。

數(shù)據(jù)是在 2020 年 4 月 22 日從知乎上獲得的,整理成了 csv 文件滞欠,可以在 此處 查看古胆。

這里我們使用了的命令行處理工具是 structopt,它是基于 clap 封裝而成筛璧,可以將命令行參數(shù)通過(guò)一個(gè)結(jié)構(gòu)體來(lái)表達(dá)逸绎,并且通過(guò)過(guò)程宏來(lái)生成 clap 命令行解析器,使用起來(lái)非常簡(jiǎn)單夭谤。下面來(lái)看看命令行參數(shù)結(jié)構(gòu)體定義:

use structopt::StructOpt;

/// Opt collects the command line arguments
#[derive(Debug, StructOpt)]
#[structopt(name = env!("CARGO_PKG_NAME"))]
#[structopt(version = env!("CARGO_PKG_VERSION"))]
#[structopt(about = env!("CARGO_PKG_DESCRIPTION"))]
#[structopt(author = env!("CARGO_PKG_AUTHORS"))]
struct Opt {
    /// Query data of which country
    #[structopt(value_name = "COUNTRY")]
    country: String,

    /// Input data file
    #[structopt(long, short, parse(from_os_str), value_name = "DATA_PATH")]
    data_path: Option<PathBuf>,

    /// Don't show noisy messages
    #[structopt(long, short)]
    quiet: bool,
}

緊接著是我們需要根據(jù) csv 文件編寫用于表達(dá)記錄的結(jié)構(gòu)體棺牧,具體代碼如下:

/// Record represents a row in the target csv file
#[derive(Debug, Deserialize)]
struct Record {
    country: String,
    number_of_newly_diagnosis: u32,
    number_of_cumulative_diagnosis: u32,
    number_of_current_diagnosis: u32,
    number_of_deaths: u32,
    number_of_cures: u32,
}

錯(cuò)誤類型的話,在上面的示例也做過(guò)分析朗儒,基本類似颊乘,所以這里直接給出自定義錯(cuò)誤類型如下,不過(guò)為了避免手動(dòng)實(shí)現(xiàn) Error trait醉锄,以及一些 From 實(shí)現(xiàn)乏悄,這里使用了 thiserror,這樣可以讓代碼更加緊湊:

#[derive(Error, Debug)]
enum CliError {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
    Csv(#[from] csv::Error),
    #[error("no matching record found")]
    NotFound,
}

然后是我們的核心業(yè)務(wù)邏輯 search 函數(shù)實(shí)現(xiàn):

fn search<P: AsRef<Path>>(input: &Option<P>, country: &str) -> Result<Record, CliError> {
    let input: Box<dyn io::Read> = match input {
        None => Box::new(io::stdin()),
        Some(p) => Box::new(fs::File::open(p)?),
    };
    let mut rdr = csv::Reader::from_reader(input);
    for r in rdr.deserialize() {
        let record: Record = r?;
        if record.country == country {
            return Ok(record);
        }
    }

    Err(CliError::NotFound)
}

最后在 main 函數(shù)中把以上串起來(lái)即可:

fn main() {
    let opt = Opt::from_args();
    match search(&opt.data_path.map(|x| x.as_path().to_owned()), &opt.country) {
        Ok(r) => println!("{:?}", r),
        Err(e) => {
            println!("{}", e);
            process::exit(1);
        }
    }
}

代碼寫完后恳不,我們可以通過(guò)運(yùn)行 cargo run -- --help 查看使用幫助如下:

covid 0.1.0
0xE8551CCB <noti@ifaceless.space>
A handful cli to query covid-19 infections in the world.

USAGE:
    covid [FLAGS] [OPTIONS] <COUNTRY>

FLAGS:
    -h, --help       Prints help information
    -q, --quiet      Don't show noisy messages
    -V, --version    Prints version information

OPTIONS:
    -d, --data-path <DATA_PATH>    Input data file

ARGS:
    <COUNTRY>    Query data of which country

使用示例如下:

$ cargo run -- -d=assets/covid-19-infections-20200422.csv 美國(guó)
Record { country: "美國(guó)", number_of_newly_diagnosis: 36386, number_of_cumulative_diagnosis: 825306, number_of_current_diagnosis: 704558, number_of_deaths: 45075, number_of_cures: 75673 }

$ cargo run -- -d=assets/covid-19-infections-20200422.csv 不存在
no matching record found

總結(jié)

好啦檩小,終于翻譯完了,內(nèi)容很多烟勋,不過(guò)我們只要把握好主線即可快速掌握作者想要給我們傳達(dá)的信息规求。

原文

聲明

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鸵荠,隨后出現(xiàn)的幾起案子冕茅,更是在濱河造成了極大的恐慌,老刑警劉巖蛹找,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姨伤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡庸疾,警方通過(guò)查閱死者的電腦和手機(jī)乍楚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)届慈,“玉大人徒溪,你說(shuō)我怎么就攤上這事忿偷。” “怎么了臊泌?”我有些...
    開(kāi)封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵鲤桥,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我渠概,道長(zhǎng)茶凳,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任播揪,我火速辦了婚禮贮喧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猪狈。我一直安慰自己箱沦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布罪裹。 她就那樣靜靜地躺著饱普,像睡著了一般运挫。 火紅的嫁衣襯著肌膚如雪状共。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天谁帕,我揣著相機(jī)與錄音峡继,去河邊找鬼。 笑死匈挖,一個(gè)胖子當(dāng)著我的面吹牛碾牌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播儡循,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼舶吗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了择膝?” 一聲冷哼從身側(cè)響起誓琼,我...
    開(kāi)封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肴捉,沒(méi)想到半個(gè)月后腹侣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡齿穗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年傲隶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窃页。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跺株,死狀恐怖复濒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情乒省,我是刑警寧澤芝薇,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站作儿,受9級(jí)特大地震影響洛二,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜攻锰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一晾嘶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧娶吞,春花似錦垒迂、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至绣夺,卻和暖如春吏奸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背陶耍。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工奋蔚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人烈钞。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓泊碑,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親毯欣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子馒过,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355