引言
本文內(nèi)容主要翻譯自 Andrew Gallant 的文章 Error Handling in Rust吼驶。
如同大多數(shù)的編程語(yǔ)言澜共,Rust 中也需要通過(guò)特定的方式處理錯(cuò)誤命雀。眾所周知,目前常見(jiàn)的錯(cuò)誤處理方式主要分為兩種:
- 異常機(jī)制(C#/Java/Python 等)珍昨;
- 返回錯(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)潔习勤。
說(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)要指南:
- 如果你是 Rust 新手党饮,對(duì)于系統(tǒng)編程和富類型系統(tǒng)(expressive type systems)不太熟悉的話,推薦你從頭開(kāi)始閱讀(如果是完全沒(méi)有了解過(guò) Rust 的童鞋驳庭,推薦先閱讀下 Rust Book刑顺;
- 如果你從來(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」部分;
- 如果你已經(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)
From
和Error
trait铺坞,從而可以順利地使用?
操作符,讓錯(cuò)誤處理更加優(yōu)雅(要是你連From
和Error
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ì)使用
Option
和Result
中定義的組合子钝诚,有時(shí)候只是使用它們可能比較索然無(wú)味,但是可以通過(guò)合理地組合使用?
操作符合組合子來(lái)改善代碼榄棵。and_then
,map
和unwrap_or
是作者比較喜歡的幾個(gè)組合子凝颇。
總結(jié)一下流程如下:
基礎(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) panic
(panic
會(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í)完 Option
和 Result
類型后自然就知道了边臼。這兩種類型都有關(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)
窄锅。Some
是 Option
類型的值構(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_then
在 Result
類型中都可以使用盖呼。此外儒鹿,Result
還有第二個(gè) Err
類型,所以還有一些專門關(guān)聯(lián)錯(cuò)誤類型的組合子几晤,例如 map_err
和 or_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ò)誤的方式查蓉。但是,如果 Option
和 Result
同時(shí)出現(xiàn)呢榜贴?又或者我們同時(shí)遇到 Result<T, Error1>
和 Result<T, Error2>
豌研?接下來(lái)的挑戰(zhàn)就是處理多種組合的錯(cuò)誤類型,這也是本文的核心內(nèi)容唬党。
組合 Option
和 Result
至此鹃共,我們已經(jīng)學(xué)習(xí)了很多關(guān)于 Option
h 和 Result
的組合子,并且可以使用這些組合子將不同的計(jì)算結(jié)果組合起來(lái)返回驶拱,不需要顯式地編寫分支判斷邏輯霜浴。
然而,現(xiàn)實(shí)代碼并非如此干凈蓝纲。有時(shí)阴孟,我們會(huì)有 Option
和 Result
類型的混合,這時(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ā)生:
- 打開(kāi)文件摘悴;
- 從文件中讀取數(shù)據(jù)峭梳;
- 將文本數(shù)據(jù)轉(zhuǎn)換成數(shù)字。
前兩個(gè)錯(cuò)誤可以使用 std::io::Error
類型(這個(gè)可以從 std::fs::File::open
和 std::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
, map
和 map_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):
- 容易污染代碼屈藐,搞得到處都是錯(cuò)誤消息字符串榔组;
- 字符串會(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:
-
std::error::Error
,專門用于表示錯(cuò)誤玫芦; -
std::convert::From
浆熔,更加通用的接口,用于兩種類型之間的轉(zhuǎn)換桥帆。
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);
上面的 err1
和 err2
的類型都是相同的類型,這里都是 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í)提供了如下幾種能力:
- 分支判斷摩桶;
- 控制流桥状;
- 錯(cuò)誤類型轉(zhuǎn)換帽揪。
但是硝清,還有一個(gè)小瑕疵沒(méi)有解決。Box<Error>
是模糊的转晰,我們沒(méi)法得知原先的錯(cuò)誤類型芦拿,雖然相比于 String
,我們可以通過(guò) description
和 cause
獲取到更詳細(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::Debug
和 fmt::Display
)袱瓮。
此外缤骨,我們也需要為自定義錯(cuò)誤類型提供 From
的實(shí)現(xiàn),這樣就可以組合更多詳細(xì)錯(cuò)誤了尺借。比如荷憋,csv::Error
就提供了來(lái)自 io::Error
和 byteorder::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)用的地方):
-
fs::File::open
會(huì)返回io::Error
煌往; -
csv::Reader::decode
每次解碼一條記錄,該操作會(huì)產(chǎn)生csv::Error
(參見(jiàn)Iterator
實(shí)現(xiàn)中關(guān)聯(lián)的Item
類型)轧邪; - 如果
row.population
為None
刽脖,那么調(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)整即可:
- 調(diào)整程序可以接收一個(gè)城市參數(shù)占哟,而人口數(shù)據(jù)可以從
stdin
中讀刃氖; - 修改
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á)的信息规求。
原文
聲明
- 本文鏈接: https://ifaceless.github.io/2020/06/02/rust-error-handling
- 版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協(xié)議神妹。轉(zhuǎn)載請(qǐng)注明出處颓哮!