<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 錯誤處理 > [error-handling.md](https://github.com/rust-lang/rust/blob/master/src/doc/book/error-handling.md) commit e26279db48cc5510a13f0e97bde97ccd2d2a1854 就像大多數編程語言,Rust 鼓勵程序猿以特定的方式處理錯誤。一般來講,錯誤處理被分割為兩個大類:異常和返回值。Rust 選擇了返回值。 在這一部分,我們試圖提供一個全面的 Rust 如何處理錯誤的解決方案。不僅如此,我們也嘗試一次一點的介紹錯誤處理,這樣當你離開時會有一個所有東西如何協調的堅實理解。 Rust 的錯誤處理天生是冗長而煩人的。這一部分將會探索這些坑并展示如何使用標準庫來讓錯誤處理變得準確和符合工程原理。 ### 內容列表 這一部分灰常的長,大部分因為我們從最基礎的和類型和組合入手,并嘗試一點一點的解釋 Rust 錯誤處理的動機。為此,對有其他類似類型系統經驗的童鞋可能想要跳過一些內容。 - [基礎](#) - [理解 unwrapping](#) - [`Option`類型](#) - [組合`Option<T>`值](#) - [`Result`類型](#) - [解析整型](#) - [`Result`類型別名習慣](#) - [小插曲:unwrapping 并不邪惡](#) - [處理多種錯誤類型](#) - [組合`Option`和`Result`](#) - [組合的限制](#) - [提早返回](#) - [`try!`宏](#) - [定義你自己的錯誤類型](#) - [用于錯誤處理的標準庫 trait](#) - [`Error`trait](#) - [`From`trait](#) - [真正的`try!`宏](#) - [組合自定義錯誤類型](#) - [給庫編寫者的建議](#) - [案例學習:一個讀取人口數據的程序](#) - [初始化](#) - [參數解析](#) - [編寫邏輯](#) - [使用`Box<Error>`處理錯誤](#) - [從標準輸入讀取](#) - [用自定義類型處理錯誤](#) - [增加功能](#) - [精簡版](#) ### 基礎 你可以認為錯誤處理是用事例分析(case analysis)來決定一個計算成功與否。如你所見,工程性的錯誤處理就是要減少程序猿顯式的事例分析的同時保持代碼的可組合性。 保持代碼的可組合性是很重要的,因為沒有這個要求,我們可能在遇到沒想到的情況時[panic](http://doc.rust-lang.org/std/macro.panic!.html)。(`panic`導致當前線程結束,而在大多數情況,導致整個程序結束。)這是一個例子: ~~~ // Guess a number between 1 and 10. // If it matches the number we 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); } ~~~ 如果你運行這段代碼,程序會崩潰并輸出類似如下信息: ~~~ thread '' panicked at 'Invalid number: 11', src/bin/panic-simple.rs:5 ~~~ 這是另一個稍微不那么違和的例子。一個接受一個整型作為參數,乘以二并打印的程序。 ~~~ 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); } ~~~ 如果你給這個程序 0 個參數(錯誤 1)或者 第一個參數并不是整型(錯誤 2),這個程序也會像第一個例子那樣 panic。 你可以認為這種風格的錯誤處理類似于沖進瓷器店的公牛。它會沖向任何它想去的地方,不過會毀掉過程中的一切。 ### 理解 unwrapping 在之前的例子中,我們聲稱程序如果遇到兩個錯誤情況之一會直接 panic,不過,程序并不像第一個程序那樣包括一個顯式的`panic`調用。這是因為 panic 嵌入到了`unwrap`的調用中。 Rust 中“unwrap”是說,“給我計算的結果,并且如果有錯誤,panic 并停止程序。”因為他們很簡單如果我們能展示 unwrap 的代碼就更好了,不過在這么做之前,我們首先需要探索`Option`和`Result`類型。他們倆都定義了一個叫`unwrap`的方法。 ### `Option`類型 `Option`類型[定義在標準庫中](http://doc.rust-lang.org/std/option/enum.Option.html): ~~~ enum Option<T> { None, Some(T), } ~~~ `Option`類型是一個 Rust 類型系統用于表達*不存在的可能性(possibility of absence)*的方式。將不存在的可能性編碼進類型系統是一個重要概念,因為它會強迫編譯器處理不存在的情況。讓我們看看一個嘗試在一個字符串中找一個字符的例子: ~~~ // 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 } ~~~ 注意當函數找到一個匹配的字符,它并不僅僅返回`offset`。相反,它返回`Some(offset)`。`Some`是一個`Option`類型的一個變體或一個值構造器。你可以認為它是一個`fn<T>(value: T) -> Option<T>`類型的函數。同理,`None`也是一個值構造器,除了它并沒有參數。你可以認為`None`是一個`fn<T>() -> Option<T>`類型的函數。 這可能看起來并沒有什么,不過這是故事的一半。另一半是使用我們編寫的`find`函數。讓我們嘗試用它查找文件名的擴展名。 ~~~ # fn find(_: &str, _: char) -> Option<usize> { None } fn main() { let file_name = "foobar.rs"; match find(file_name, '.') { None => println!("No file extension found."), Some(i) => println!("File extension: {}", &file_name[i+1..]), } } ~~~ 這段代碼使用[模式識別](#)來對`find`函數的返回的`Option<usize>`進行 case analysis。事實上,case analysis 是唯一能獲取`Option<T>`中存儲的值的方式。這意味著你,作為一個程序猿,必須處理當`Option<T>`是`None`而不是`Some(t)`的情況。 不過稍等,那我們[之前](#)使用的`unwrap`呢?那里并沒有 case analysis!相反,case analysis 被放入了`unwrap`方法中。如果你想的話你可以自己定義它: ~~~ 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`方法抽象出了 case analysis。這正是`unwrap`的工程化用法。不幸的是,`panic!`意味著`unwrap`并不是可組合的:它是瓷器店中的公牛。 #### 組合`Option<T>`值 在[之前的例子](#)中,我們看到了如何用`find`發現文件名的擴展名。當然,并不是所有文件名都有一個`.`,所以可能文件名并沒有擴展名。不存在的可能性被編碼進了使用`Option<T>`的類型。換句話說,編譯器將會強制我們描述一個擴展名不存在的可能性。在我們的例子中,我們只打印出一個說明情況的信息。 獲取一個文件名的擴展名是一個很常見的操作,所以把它放進一個函數是很有道理的: ~~~ # fn find(_: &str, _: char) -> Option<usize> { None } // Returns the extension of the given file name, where the extension is defined // as all characters proceeding 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..]), } } ~~~ (專業建議:不要用這段代碼,相反使用標準庫的[extension](http://doc.rust-lang.org/std/path/struct.Path.html#method.extension)方法。) 代碼是簡單的,不過重要的是注意到`find`的類型強迫我們考慮不存在的可能性。這是一個好事,因為這意味著編譯器不會讓我們不小心忘記了文件名沒有擴展名的情況。另一方面,每次都像`extension_explicit`那樣進行顯式 case analysis 會變得有點無聊。 事實上,`extension_explicit`的 case analysis 遵循一個非常常見的模式:將`Option<T>`中的值映射為一個函數,除非它是`None`,這時,返回`None`。 Rust 擁有參數多態(parametric polymorphism),所以定義一個組合來抽象這個模式是很容易的: ~~~ 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)), } } ~~~ 事實上,`map`是標準庫中的`Option<T>`[定義的一個方法](http://doc.rust-lang.org/std/option/enum.Option.html#method.map)。 用我們的新組合,我們可以重寫我們的`extension_explicit`方法來去掉 case analysis: ~~~ # fn find(_: &str, _: char) -> Option<usize> { None } // Returns the extension of the given file name, where the extension is defined // as all characters proceeding 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..]) } ~~~ 我們通常會發現的另一個模式是為一個`Option`為`None`時賦一個默認值。例如,也許你的程序假設即便一個文件沒有擴展名則它的擴展名是`rs`。正如你可能想象到的,這里的 case analysis 并不特定用于文件擴展名 - 它可以用于任何`Option<T>`: ~~~ fn unwrap_or<T>(option: Option<T>, default: T) -> T { match option { None => default, Some(value) => value, } } ~~~ 這里要注意的是默認值的類型必須與可能出現在`Option<T>`中的值類型相同。在我們的例子中使用它是非常簡單的: ~~~ # fn find(haystack: &str, needle: char) -> Option<usize> { # for (offset, c) in haystack.char_indices() { # if c == needle { # return Some(offset); # } # } # None # } # # fn extension(file_name: &str) -> Option<&str> { # find(file_name, '.').map(|i| &file_name[i+1..]) # } fn main() { assert_eq!(extension("foobar.csv").unwrap_or("rs"), "csv"); assert_eq!(extension("foobar").unwrap_or("rs"), "rs"); } ~~~ (注意`unwrap_or`是標準庫中的`Option<T>`[定義的一個方法](http://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or),所以這里我們使用它而不是我們上面定義的獨立的函數。別忘了看看更通用的[`unwrap_or_else`](http://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or_else)方法。) 還有另一個我們認為值得特別注意的 組合:`and_then`。它讓我們更容易的組合不同的承認不存在的可能性的計算。例如,這一部分的很多代碼是關于找到一個給定文件的擴展名的。為此,你首先需要一個通常截取自文件路徑的文件名。雖然大部分文件路徑都有一個文件名,但并不是都有。例如,`.`,`..`,`/`。 所以,我們面臨著從一個給定的文件路徑找出一個擴展名的挑戰。讓我們從顯式 case analysis 開始: ~~~ # fn extension(file_name: &str) -> Option<&str> { None } 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!() } ~~~ 你可能認為我們應該用`map`組合來減少 case analysis,不過它的類型并不匹配。也就是說,`map`獲取一個只處理(Option)內部值的函數。這導致那個函數總是[重新映射成了`Some`](#)。因此,我們需要一些類似`map`,不過允許調用者返回另一個`Option`的方法。它的泛型實現甚至比`map`更簡單: ~~~ 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), } } ~~~ 現在我們可以不用顯式 case analysis 重寫我們的`file_path_ext`函數了: ~~~ # fn extension(file_name: &str) -> Option<&str> { None } # fn file_name(file_path: &str) -> Option<&str> { None } fn file_path_ext(file_path: &str) -> Option<&str> { file_name(file_path).and_then(extension) } ~~~ `Option`類型有很多其他[定義在標準庫中的](http://doc.rust-lang.org/std/option/enum.Option.html)組合。過一邊這個列表并熟悉他們的功能是一個好主意 —— 通常他們可以減少你的 case analysis。熟悉這些組合將會得到回報,因為他們很多也為`Result`類型定義了實現(相似的語義),而我們接下來會講`Result`。 組合使用像`Option`這樣的符合工程學的類型來減少顯式 case analysis。他們也是可組合的因為他們允許調用者以他們自己的方式處理不存在的可能性。像`unwrap`這樣的方法去掉了選擇因為當`Option<T>`為`None`他們會 panic。 ### `Result`類型 `Result`類型也[定義于標準庫中](http://doc.rust-lang.org/std/result/index.html): ~~~ enum Result<T, E> { Ok(T), Err(E), } ~~~ `Result`是`Option`的高級版本。相比于像`Option`那樣表示不存在的可能性,`Result`表示錯誤的可能性。通常,錯誤用來解釋為什么一些計算會失敗。嚴格的說這是一個更通用的`Option`。考慮如下類型別名,它的語義在任何地方都與真正的`Option<T>`相同: ~~~ type Option<T> = Result<T, ()>; ~~~ 它把`Result`的第二個類型參數改為總是`()`(讀作“單元”或“空元組”)。`()`類型只有一個值:`()`(沒錯,類型和值級別的形式是一樣的!) `Result`類型是一個代表一個計算的兩個可能結果的方式。通常,一個結果是期望的值或者“`Ok`”而另一個意味著非預期的或者“`Err`”。 就像`Option`,`Result`在標準庫中也[定義了一個`unwrap`方法](http://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap)。讓我們定義它: ~~~ # enum Result<T, E> { Ok(T), Err(E) } 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), } } } ~~~ 這實際上與[`Option::unwrap`的定義](#)一樣,除了它在`panic!`信息中包含了錯誤信息。這讓調試變得簡單,不過也要求我們為`E`類型參數(它代表我們的錯誤類型)添加一個[`Debug`](http://doc.rust-lang.org/std/fmt/trait.Debug.html)限制。因為絕大部分類型應該滿足`Debug`限制,這使得它可以在實際中使用。(`Debug`簡單的意味著這個類型有合理的方式可以打印出人類可讀的描述。) OK,讓我們開始一個例子。 #### 解析整型 Rust 標準庫中讓字符串轉換為整型變得異常簡單。事實上它太簡單了,以至于你可以寫出如下代碼: ~~~ 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); } ~~~ 在這里,你應該對調用`unwrap`持懷疑態度。例如,如果字符串并不能解析為一個數字,它會 panic: ~~~ thread '' 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 ~~~ 這是很難堪的,而且如果這在你所使用的庫中出現了的話,可以理解你會很煩躁。相反,我們應該嘗試在我們的函數里處理錯誤并讓調用者決定該怎么做。這意味著改變`double_number`的返回值類型。不過改編成什么呢?好吧,這需要我們看看標準庫中[`parse`方法](http://doc.rust-lang.org/std/primitive.str.html#method.parse)的簽名: ~~~ impl str { fn parse<F: FromStr>(&self) -> Result<F, F::Err>; } ~~~ 額嗯。所以至少我們知道了我們需要使用一個`Result`。當然,也可以返回一個`Option`。畢竟,一個字符串要么能解析成一個數字要么不能,不是嗎?這當然是一個合理的方式,不過實現內部區別了為什么字符串不能解析成數字。(要么是一個空字符串,一個無效的數位,太大或太小。)因此,使用`Result`更有道理因為我們想要比單純的“不存在”提供更多信息。我們想要表明*為什么*解析會失敗。你應該嘗試再現這樣的推理,當你面對一個`Option`和`Result`之間的選擇時。如果你可以提供詳細的錯誤信息,那么大概你也應該提供。(我們會在后面詳細講到。) 好的,不過我們的返回值類型該怎么寫呢?上面定義的`parse`方法對所有不同的標準庫定義的數字類型是泛型的。我們也可以(應該)讓我們的函數也是泛型的,不過這回讓我們享受顯式定義的好處。我們只關心`i32`,所以我們需要尋找[`FromStr`的實現](http://doc.rust-lang.org/std/primitive.i32.html)(在你的瀏覽器中用`CTRL-F`搜索“FromStr”)和[與它相關的類型`](#)`Err`。這么做我可以找出具體的錯誤類型。在這個例子中,它是[`std::num::ParseIntError`](http://doc.rust-lang.org/std/num/struct.ParseIntError.html)。最后我們可以重寫函數: ~~~ 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), } } ~~~ 這比之前有些進步,不過現在我們寫的代碼有點多了!case analysis 又一次坑了我們。 組合是救星!就像`Option`一樣,`Result`有很多定義的組合方法。`Result`和`Option`之間的常用組合有很大的交集。特別的,`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), } } ~~~ 常見的組合`Result`都有,包括[unwrap_or](http://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or)和[and_then](http://doc.rust-lang.org/std/result/enum.Result.html#method.and_then)。另外,因為`Result`有第二個類型參數,所以有一些只影響錯誤類型的組合,例如[map_err](http://doc.rust-lang.org/std/result/enum.Result.html#method.map_err)(相對于`map`)和[or_else](http://doc.rust-lang.org/std/result/enum.Result.html#method.or_else)(相對于`and_then`)。 #### `Result`類型別名習慣 在標準庫中,你可能經常看到像`Result<i32>`這樣的類型。不過等等,[我們定義的`Result`](#)有兩個類型參數。我么怎么能只指定一個呢?這里的關鍵是定義一個`Result`類型別名來對一個特定類型固定其中一個類型參數。通常固定的類型是錯誤類型。例如,我們之前的解析整數例子可以重寫成這樣: ~~~ use std::num::ParseIntError; use std::result; type Result<T> = result::Result<T, ParseIntError>; fn double_number(number_str: &str) -> Result<i32> { unimplemented!(); } ~~~ 為什么我們應該這么做?好吧,如果我們有很多可能返回`ParseIntError`的函數,那么定義一個總是使用`ParseIntError`的別名就比每回都寫一遍要方便很多。 這個習慣最突出的一點是標準庫中的[`io::Result`](http://doc.rust-lang.org/std/io/type.Result.html)。通常,當你使用`io::Result<T>`,很明顯你就是在使用`io`模塊的類型別名而不是`std::result`的原始定義。(這個習慣也用于[`fmt::Result`](http://doc.rust-lang.org/std/fmt/type.Result.html)。) ### 小插曲:unwrapping 并不邪惡 如果你一路跟了過來,你可能注意到我們花了很大力氣反對使用像`unwrap`這樣會`panic`并終止你的程序的方法。通常來說,這是一個好的建議。 然而,`unwrap`仍然可以被明智的使用。具體如何正當化`unwrap`的使用是一個灰色區域并且理性的人可能不會同意。我會簡述這個問題的一些個人看法。 - **在例子和簡單快速的編碼中。**有時你要寫一個例子或小程序,這時錯誤處理一點也不重要。這種情形要擊敗`unwrap`的方便易用是很難的,所以它顯得很合適。 - **當 panic 就意味著程序中有 bug 的時候。**當你代碼中的不變量應該阻止特定情況發生的時候(比如,從一個空的棧上彈出一個值),那么 panic 就是可行的。這是因為它暴露了程序的一個 bug。這可以是顯式的,例如一個`assert!`失敗,或者因為一個數組越界。 這可能并不是一個完整的列表。另外,當使用`Option`的時候,通常使用[`expect`](http://doc.rust-lang.org/std/option/enum.Option.html#method.expect)方法更好。`expect`做了`unwrap`同樣的工作,除了`expect`會打印你給它的信息。這讓 panic 的結果更容易處理,因為相比“called unwrap on a None value.”會提供一個信息。 我的建議濃縮如下:運用你良好的判斷。我的文字中并沒有出現“永遠不要做 X”或“Y 被認為是有害的”是有原因的。所有這些都是權衡取舍,并且這是你們程序猿的工作去決定在你的用例中哪個是可以接受的。我們目標只是盡可能的幫助你進行權衡。 現在我們介紹了 Rust 的基本的錯誤處理,并解釋了 unwrap,讓我們開始更多的探索標準庫。 ### 處理多種錯誤類型 到目前為止,我看到了不是`Option<T>`就是`Result<T, SomeError>`的錯誤處理。不過當你同時使用`Option`和`Result`時會發生什么呢?或者如果你使用`Result<T, Error1>`和`Result<T, Error2>`呢?我們接下來的挑戰是處理*不同錯誤類型的組合*,這將會是貫穿本章余下部分的主要主題。 ### 組合`Option`和`Result` 到目前為止,我們講到了為`Option`和`Result`定義的組合。我們可以用這些組合來處理不同的計算結果而不用進行顯式的 case analysis。 當然,在實際的代碼中,事情并不總是這么明顯。有時你遇到一個`Option`和`Result`的混合類型。我們是必須求助于顯式 case analysis,或者我們可以使用組合呢? 現在,讓我們重溫這一部分的第一個例子: ~~~ 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); } ~~~ 基于我們新掌握的關于`Option`,`Result`和他們的組合的知識,我們應該嘗試重寫它來適當的處理錯誤這樣出錯時程序就不會 panic 了。 這里的坑是`argv.nth(1)`產生一個`Option`而`arg.parse()`產生一個`Result`。他們不是直接可組合的。當同時遇到`Option`和`Result`的時候,解決辦法通常是把`Option`轉換為一個`Result`。在我們的例子中,缺少命令行參數(來自`env::args()`)意味著我們的用戶沒有正確調用我們的程序。我們可以用一個`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())) .map(|n| 2 * n) } fn main() { match double_arg(env::args()) { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } } ~~~ 這個例子中有幾個新東西。第一個是使用了[`Option::ok_or`](http://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or)組合。這是一個把`Option`轉換為`Result`的方法。這個轉換要求你指定當`Option`為`None`時的錯誤。就像我們見過的其他組合一樣,它的定義是非常簡單的: ~~~ fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> { match option { Some(val) => Ok(val), None => Err(err), } } ~~~ 另一個新使用的組合是[`Result::map_err`](http://doc.rust-lang.org/std/result/enum.Result.html#method.map_err)。這就像`Result::map`,除了它映射一個函數到`Result`的 error 部分。如果`Result`是一個`Ok(...)`,那么它什么也不修改。 這里我們使用`map_err`是因為它對保證相同的錯誤類型(因為我們使用了`and_then`)是必要的。因為我們選擇把`Option<String>`(來自`argv.nth(1)`)轉換為`Result<String, String>`,我們也必須把來自`arg.parse()`的`ParseIntError`轉換為`String`。 ### 組合的限制 IO 和 解析輸入是非常常見的任務,這也是我個人在 Rust 經常做的。因此,我們將使用(并一直使用)IO 和多種解析工作作為例子講解錯誤處理。 讓我們從簡單的開始。我們的任務是打開一個文件,讀取所有的內容并把他們轉換為一個數字。接著我們把它乘以`2`并打印結果。 雖然我們勸告過你不要用`unwrap`,不過開始寫代碼的時候`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); } ~~~ (附注:`AsRef<Path>`被使用是因為它與[`std::fs::File::open`有著相同的 bound](http://doc.rust-lang.org/std/fs/struct.File.html#method.open)。這讓我們可以用任何類型的字符串作為一個文件路徑。) 這里可能出現三個不同錯誤: 1. 打開文件出錯。 1. 從文件讀數據出錯。 1. 將數據解析為數字出錯。 頭兩個錯誤被描述為[`std::io::Error`](http://doc.rust-lang.org/std/io/struct.Error.html)類型。我們知道這些因為返回類型是[`std::fs::File::open`](http://doc.rust-lang.org/std/fs/struct.File.html#method.open)和[`std::io::Read::read_to_string`](http://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string)。(注意他們都使用了之前描述的[`Result`類型別名習慣](#)。如果你點擊`Result`類型,你將會[看到這個類型別名](http://doc.rust-lang.org/std/io/type.Result.html),以及底層的`io::Error`類型。)第三個問題被描述為[`std::num::ParseIntError`](http://doc.rust-lang.org/std/num/struct.ParseIntError.html)。特別的`io::Error`被廣泛的用于標準庫中。你會一次又一次的看到它。 讓我們著手重構`file_double`函數。為了讓這個函數可以與程序的其他組件組合,它必須不能在上述錯誤情況下 panic。事實上,這意味著它在任何操作失敗時應該返回一個錯誤。我們的問題是`file_double`的返回類型是`i32`,它并沒有給我們一個有效的報告錯誤的途徑。因此,我們必須以把返回類型`i32`改成別的什么的開始。 我們需要決定的第一件事:我們應該用`Option`還是`Result`?當然我們可以簡單的選擇`Option`。如果出現任何錯誤了,我們可以簡單的返回`None`。它可以工作并且比 panic 好多了,不過我們可以做的更好。我們應該在錯誤發生時傳遞一些細節。因為我們想要表達*錯誤的可能性*,我們應該使用`Result<i32, E>`。不過`E`應該是什么呢?因為可能出現兩種不同的錯誤,我們需要把他們轉換為一種通用類型。其中之一就是`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), } } ~~~ 這些代碼看起來有點難以理解。在能輕松編寫這樣的代碼前可能需要更多一些實踐。我們寫代碼的方式遵循*跟著類型走(following the types)*。一旦我們把`file_double`的返回類型改為`Result<i32, String>`,我們就不得不開始尋找正確的組合。在這個例子中,我們只用到了三個不同的組合:`and_then`,`map`和`map_err`。 `and_then`被用來連接多個計算,其中每一個都有可能返回一個錯誤。在打開文件后,還有另外兩個可能失敗的計算:從文件讀取和把內容解析成數字。相應地,有兩個`and_then`的調用。 `map`用來把一個函數用于`Result`的`Ok(...)`值。例如,最后一個`map`調用把`Ok(...)`值(它是一個`i32`)乘以`2`。如果在這之前出現了錯誤,這里的操作會被省略,因為`map`是這么定義的。 `map_err`是讓一切可以工作的關鍵。`map_err`類似`map`,除了它把一個函數用于`Result`的`Err(...)`值。在這個例子中,我們想要把所有的錯誤轉換為一個類型:`String`。因為`io::Error`和`num::ParseIntError`都實現了`ToString`,我們可以調用`to_string()`去轉換他們。 說了這么多,代碼仍然不好懂。掌握組合的應用是很重要的,不過他們也有限制。讓我們嘗試一個不同的方式:提早返回。 ### 提早返回 我想利用前一章節的代碼并用提早返回重寫它。提早返回讓你提前退出函數。我們不能在另一個閉包中從`file_double`提前返回,所以我們需要退回到顯式 case analysis。 ~~~ 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), } } ~~~ 理性的同學可能不同意這個比使用組合的代碼更好,不過如果你并不熟悉組合方式,這些代碼閱讀起來更簡單。它通過`match`和`if let`進行了顯式 case analysis。如果錯誤出現了,它簡單的停止執行并返回錯誤(通過轉換為一個字符串)。 這難道不是倒退嗎?之前,我們說過工程性的錯誤處理的關鍵是減少顯式 case analysis,不過這里我們又退回到了顯式 case analysis。這表明,有多種方式可以減少顯式 case analysis。組合并不是唯一的方法。 ### `try!`宏 Rust 中錯誤處理的基石是`try!`宏。`try!`宏像組合一樣抽象了 case analysis,不過不像組合,它也抽象了控制流。也就是說,它可以抽象我們之前看到的提早返回的模式。 這是一個簡單化的`try!`宏定義: ~~~ macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); } ~~~ ([實際的定義](http://doc.rust-lang.org/std/macro.try!.html)有一點復雜。我們會在后面詳述。) 使用`try!`宏讓我們的最后的例子異常的簡單。因為它為我們做了 case analysis 和提早返回,我們的代碼更緊湊也更易于理解: ~~~ 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), } } ~~~ 根據[我們`try!`宏的定義](#)`map_err`調用仍是必要的。這是因為錯誤類型仍然需要被轉換為`String`。好消息是我們馬上就會學到如何移除這些`map_err`調用!壞消息是在我們可以移除`map_err`調用之前我們需要更深入的學習一些標準庫中的重要的 trait。 ### 定義你自己的錯誤類型 在我們深入學習一些標準庫錯誤 trait 之前,我想要通過移除之前例子中的作為錯誤類型的`String`來結束本章節。 之前我們的例子中使用`String`是為了方便,因為把錯誤轉換為字符串是簡單的,甚至把我們自己的類型轉換為字符串也是如此。然而,把`String`作為你的錯誤有一些缺點。 第一個缺點是錯誤信息會傾向于另你的代碼變得凌亂。把錯誤信息定義在別處是可能的,不顧除非你非常的(qiang)自(po)律(zheng),你很容易就會把錯誤信息嵌入到你的代碼中。事實上,我們[上一個例子](#)就是這么做的。 第二個也是更重要的缺點是`String`是*不完整的*。也就是說,如果所有錯誤都轉換成了字符串,那么傳遞給調用者的錯誤就變得完全不透明了。調用者對于一個`String`類型的錯誤所能作的唯一可行的事就是把它展示給用戶。當然,通過觀察字符串來確定錯誤的類型是不健壯的。(對于一個庫來說這個缺點公認要比在例如程序中來的更重要。) 例如,`io::Error`類型內嵌了一個[`io::ErrorKind`](http://doc.rust-lang.org/std/io/enum.ErrorKind.html),它是一個表示在 IO 操作期間錯誤信息的*結構化類型*。這很重要因為你可能根據錯誤做出不同的反應。(例如,`BrokenPipe`錯誤可能意味著可以溫和的退出程序,而`NotFound`則意味著應該返回一個錯誤碼并向用戶展示錯誤。)通過`io::ErrorKind`,調用者可以用 case analysis 檢查錯誤的類型,這完全優于嘗試從`String`中梳理錯誤的細節。 除了在我們之前從文件讀取一個數字的例子中使用`String`作為錯誤類型外,我們可以定義我們自己的錯誤類型來用結構化數據代表錯誤。我們盡量不丟掉底層錯誤的信息以防調用者想要檢視細節。 表示多種可能性的理想方法是用`enum`來定義我們的集合類型。在我們的例子里,錯誤要么是`io::Error`要么是`num::ParseIntError`,所以自然的定義如下: ~~~ 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), } ~~~ 修改我們代碼非常簡單。與其把錯誤轉為字符串,我們簡單的用相應的值構造器把錯誤轉換為我們的`CliError`類型: ~~~ # #[derive(Debug)] # enum CliError { Io(::std::io::Error), Parse(::std::num::ParseIntError) } 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 = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(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), } } ~~~ 這里唯一的修改是從`map_err(|e| e.to_string())`(它把錯誤轉換為字符串)變為`map_err(CliError::Io)`或者`map_err(CliError::Parse)`。調用者要決定報告給用戶的細節的級別。實際上,使用`String`作為錯誤類型剝奪了調用者的選擇,而用一個像`CliError`這樣的`enum`類型除了一個描述錯誤的結構化數據之外還給了調用者所有的便利。 經驗之談是定義你自己的錯誤類型,不過必要時`String`也能行,特別是你在寫一個程序時。如果你在編寫一個庫,強烈建議你定義自己的錯誤類型這樣你就不會不必要的剝奪了調用者選擇。 ### 用于錯誤處理的標準庫 trait 標準庫定義了兩個完整 trait 用于錯誤處理:[`http://doc.rust-lang.org/std/error/trait.Error.html`](#)和[`std::convert::From`](http://doc.rust-lang.org/std/convert/trait.From.html)。`Error`被專門設計為描述通用錯誤,`From` trait 更多的用于在兩個不同類型值之間轉換。 ### `Error`trait `Error` trait [定義于標準庫中](http://doc.rust-lang.org/std/error/trait.Error.html): ~~~ 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 } } ~~~ 這個 trait 非常泛用因為它被設計為為所有類型實現來代表錯誤。它被證明對編寫可組合的代碼非常有幫助,正如我們將要看到的。這個 trait 允許你至少做如下事情: - 獲取一個錯誤的`Debug`表示。 - 獲取一個錯誤的面向用戶的`Display`表示 - 獲取一個錯誤的簡短描述(通過`description`方法) - 查看錯誤的調用鏈,如果存在的話(通過`cause`方法) 頭兩個是因為`Error`要求實現`Debug`和`Display`。后兩個來自于定義于`Error`的方法。`Error`的力量來自于所有錯誤類型都實現了`Error`的事實,這意味著錯誤可以被量化一個[trait 對象](#)。表現為`Box<Error>`或`&Error`。事實上,`cause`返回一個`&Error`,它自身就是一個 trait 對象。我們將在后面再次討論`Error`作為 trait 對象的功能。 目前,展示一個實現了`Error` trait 的例子是足夠的。讓我們使用[上一部分](#)我們定義的錯誤類型: ~~~ 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), } ~~~ 這個特定的錯誤類型表示出現兩種錯誤類型的可能性:一個進行 I/O 操作的錯誤或者一個把字符串轉換為數字的錯誤。這個類型可以表示任何你想要添加的錯誤類型,通過向`enum`定義添加變量。 實現`Error`是非常直觀的。這會有很多的顯式 case analysis。 ~~~ use std::error; use std::fmt; 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(), CliError::Parse(ref err) => err.description(), } } fn cause(&self) -> Option<&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), } } } ~~~ 我們注意到這是一個非常典型的`Error`的實現:為你不同的錯誤類型做匹配并滿足`description`和`cause`定義的限制。 ### `From`trait `std::convert::From` trait [定義于標準庫中](http://doc.rust-lang.org/std/convert/trait.From.html): ~~~ trait From<T> { fn from(T) -> Self; } ~~~ 非常簡單吧?`From`很有用因為它給了我們一個通用的方式來處理從一個特定類型`T`到其他類型的轉換(在這個例子中,“其他類型”是實現的主體,或者`Self`)。`From`的核心是[標準庫提供的一系列實現](http://doc.rust-lang.org/std/convert/trait.From.html)。 這里是幾個展示`From`如何工作的小例子: ~~~ let string: String = From::from("foo"); let bytes: Vec<u8> = From::from("foo"); let cow: ::std::borrow::Cow<str> = From::from("foo"); ~~~ 好的,這么說`From`用來處理字符串轉換,那么錯誤怎么辦?他被證明是一個關鍵實現: ~~~ impl<'a, E: Error + 'a> From<E> for Box<Error + 'a> ~~~ 這個實現說任何實現了`Error`的類型,我們可以把它轉換一個 trait 對象`Box<Error>`。這可能看起來并不怎么令人吃驚,不過它在泛型環境中很有用。 記的我們之前處理的兩個錯誤嗎?`io::Error`和`num::ParseIntError`。因為他們都實現了`Error`,他們也能用于`From`: ~~~ 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 對象。尤其是,對編譯器來說他們的底層類型被抹掉了,所以編譯器認為`err1`和`err2`是完全一樣的。另外,我們用完全一樣的函數調用構建`err1`和`err2`:`From::from`。這是因為`From::from`的參數和返回值都可以重載。 這個模式很重要,因為它解決了一個我們之前遇到過的問題:它給了我們一個可靠的用相同的函數把錯誤轉換為相同類型的方法。 是時候重新看看我們的老朋友:`try!`宏了。 ### 真正的`try!`宏 之前我們展示了`try!`的定義: ~~~ macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); } ~~~ 這并不是它真正的定義。它的實際定義[位于標準庫中](http://doc.rust-lang.org/std/macro.try!.html): ~~~ macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); } ~~~ 這是一個很小但很有效的修改:錯誤值被通過`From::from`傳遞。這讓`try!`宏變得更強大了一點,因為它免費提供給你自動類型轉換。 有了更強大的`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) } ~~~ 之前,我們承諾我們可以去掉`map_err`調用。實際上,所有我們需要做的就是選一個可以用于`From`的類型。一如我們在上一個部分看到的,`From`有一個可以轉換任意錯誤類型為`Box<Error>`的實現: ~~~ 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<Error>> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n = try!(contents.trim().parse::<i32>()); Ok(2 * n) } ~~~ 我們已經非常接近理想的錯誤處理了。我們的代碼處理錯誤只造成了很小的成本,因為`try!`宏同時封裝了三個東西: - case analysis。 - 控制流。 - 錯誤類型轉換。 當結合所有這些東西,我們的代碼不再受組合,`unwrap`調用或 case analysis 的困擾了。 這里還剩一點東西:`Box<Error>`是不透明的。如果我們返回一個`Box<Error>`給調用者,調用者并不能(輕易地)觀察底層錯誤類型。當然這種情形比`String`要好,因為調用者可以調用像[`description`](http://doc.rust-lang.org/std/error/trait.Error.html#tymethod.description)和[`cause`](http://doc.rust-lang.org/std/error/trait.Error.html#method.cause)這樣的方法,不過這是有限制的:`Box<Error>`是不透明的。(附注:這并不是完全正確,因為 Rust 并沒有運行時反射,這在某些場景是有用的不過[超出了本部分的范疇](https://crates.io/crates/error)。) 是時候重寫我們的`CliError`類型并將一切連起來了。 ### 組合自定義錯誤類型 在這最后一部分,我們看看真正的`try!`宏以及如何通過調用`From::from`自動轉換錯誤類型。具體的,我們把錯誤轉換為`Box<Error>`,這是可以的,不過這個類型對調用者是不透明的。 為了修改這個問題,我們使用我們已經熟知的補救方法:一個自定義錯誤類型。再一次,這是讀取文件內容并將其轉換為數字的代碼: ~~~ use std::fs::File; use std::io::{self, Read}; use std::num; use std::path::Path; // 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), } fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) } ~~~ 注意我們仍然有`map_err`的調用。為神馬?好吧,回憶[`try!`](#)和[`From`](#)的定義。問題是這里并沒有`impl`的實現允許我們將一些像`io::Error`和`num::ParseIntError`這樣的錯誤類型轉換為我們的自定義類型`CliError`。當然,這個問題很好修改!`CliError`都是我們定義的,我們可以為其實現`From`。 ~~~ # #[derive(Debug)] # enum CliError { Io(io::Error), Parse(num::ParseIntError) } use std::io; use std::num; 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) } } ~~~ 所有這些實現都是告訴`From`如何從其他類型創建一個`CliError`。在我們的例子中,構造函數就像調用相應的值構造器那樣簡單。講道理,這確實很簡單。 最后我們可以重寫`file_double`: ~~~ # use std::io; # use std::num; # enum CliError { Io(::std::io::Error), Parse(::std::num::ParseIntError) } # 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) } # } 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 = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n: i32 = try!(contents.trim().parse()); Ok(2 * n) } ~~~ 我們做的唯一一件事就是去掉了`map_err`調用。他們不再必要因為`try!`宏對錯誤類型調用了`From::from`。這一切可以工作因為我們對所有可能出現的錯誤類型提供了`From`實現。 如果我們修改我們的`file_double`函數來進行一些其他操作,例如,把自付出轉換為浮點,那么我們需要給我們的錯誤類型增加一個新變量: ~~~ use std::io; use std::num; enum CliError { Io(io::Error), ParseInt(num::ParseIntError), ParseFloat(num::ParseFloatError), } ~~~ 并增加一個新的`From`實現: ~~~ # enum CliError { # Io(::std::io::Error), # ParseInt(num::ParseIntError), # ParseFloat(num::ParseFloatError), # } use std::num; impl From<num::ParseFloatError> for CliError { fn from(err: num::ParseFloatError) -> CliError { CliError::ParseFloat(err) } } ~~~ 一切搞定! ### 給庫編寫者的建議 如果你的庫需要報告一些自定義錯誤,那么你可能應該定義你自己的錯誤類型。由你決定是否暴露它的表示(例如[`ErrorKind`](http://doc.rust-lang.org/std/io/enum.ErrorKind.html))或者把它隱藏起來(例如[`ParseIntError`](http://doc.rust-lang.org/std/num/struct.ParseIntError.html))。不過你怎么做,相比`String`表示多少提供一些關于錯誤的信息通常是好的實踐。不過說實話,這根據使用情況大有不同。 最少,你可能應該實現[`Error`](http://doc.rust-lang.org/std/error/trait.Error.html)trait。這會給你的庫的用戶以[處理錯誤](#)的最小靈活性。實現`Error`trait 也意味著可以確保用戶能夠獲得一個錯誤的字符串表示(因為它實現了`fmt::Debug`和`fmt::Display`)。 不僅如此,為你的錯誤類型提供`From`實現也是很有用的。這允許你(庫作者)和你的用戶[組合更詳細的錯誤](#)。例如,[`csv::Error`](http://burntsushi.net/rustdoc/csv/enum.Error.html)提供了`io::Error`和`byteorder::Error`。 最后,根據你的風格,你也許想要定義一個[`Result`類型別名](#),尤其是如果你的庫定義了一個單一的錯誤類型。這被用在了標準庫的[`io::Result`](http://doc.rust-lang.org/std/io/type.Result.html)和[`fmt::Result`](http://doc.rust-lang.org/std/fmt/type.Result.html)中。 ### 案例學習:一個讀取人口數據的程序 這一部分很長,并且根據你的背景,它可能顯得更加復雜。雖然有很多示例代碼以及散文一樣的解釋,但大部分都被設計為教科書式的。那么,我們要開始點新東西了:一個案例學習。 為此,為此我們將要建立一個可以讓你查詢真實世界人口數據的命令行程序。目標是簡單的:你給出一個地點接著它會告訴你人口。雖然這很簡單,但仍有很多地方我們可能犯錯。 我們將使用的數據來自[Data Science Toolkit](https://github.com/petewarden/dstkdata)。我為這個練習準備了一些數據。你要么可以獲取[世界人口數據](http://burntsushi.net/stuff/worldcitiespop.csv.gz)(41 MB gzip 壓縮,145 MB 未壓縮)或者只使用[US 人口數據](http://burntsushi.net/stuff/uscitiespop.csv.gz)(2.2 MB gzip 壓縮,7.2 MB 未壓縮)。 直到目前為止,我們的代碼一直限制在 Rust 標準庫之內。但是對于一個像這樣的真實的任務,我們至少想要一些解析 CSV 數據,解析程序參數以及將其自動轉換為 Rust 類型的東西。為此,我們將使用[`csv`](https://crates.io/crates/csv),以及[`rustc-serialize`](https://crates.io/crates/rustc-serialize)crate。 ### 初始化 我們不打算花很多時間在使用 Cargo 創建一個項目上,因為這在 [Cargo 部分](#)和 [Cargo 文檔](http://doc.crates.io/guide.html)中已被講解。 為了從頭開始,運行`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.*" rustc-serialize = "0.*" getopts = "0.*" ~~~ 你應該已經可以運行了: ~~~ cargo build --release ./target/release/city-pop # Outputs: Hello, world! ~~~ ### 參數解析 讓我們搞定參數解析,我們不會涉及太多關于 Getopts 的細節,不過有[一些不錯的文檔](http://doc.rust-lang.org/getopts/getopts/index.html)。簡單的說就是 Getopts 生成了一個參數解析器并通過要給選項的 vector(事實是一個隱藏于一個結構體和一堆方法之下的 vector)生成了一個幫助信息。一旦解析結束,我們可以解碼程序參數到一個 Rust 結構體中。從這里我們可以互獲取 flag,實例,任何程序傳遞給我們的,以及他們都有什么參數。這是我們的程序,它有合適的`extern crate`語句以及 Getopts 的基本參數操作: ~~~ extern crate getopts; extern crate rustc_serialize; use getopts::Options; use std::env; fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <data-path> <city>", program))); } fn main() { let args: Vec<String> = env::args().collect(); let program = args[0].clone(); let mut opts = Options::new(); opts.optflag("h", "help", "Show this usage message."); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(e) => { panic!(e.to_string()) } }; if matches.opt_present("h") { print_usage(&program, opts); return; } let data_path = args[1].clone(); let city = args[2].clone(); // Do stuff with information } ~~~ 首先,我們獲取一個傳遞給我們程序的 vector。接著我們我們儲存第一個參數,因為我們知道那是程序名。當一切搞定,我們設置我們的參數 flag,在這里是一個簡單的提示信息 flag。當我們設置了參數 flag 之后,我們使用`Options.parse`解析參數列表(從 1 開始,因為 0 是程序名)。如果這成功了,我們被解析的對象賦值給`matches`,如果失敗了,我們 panic。接著,我們檢查用戶是否傳遞了幫助 flag,如果是就打印使用幫助信息。幫助信息選項是 Getopts 構建的,所以為了打印用法信息所有我們需要做的就是告訴它我們想要打印什么名字和模板。如果用戶并沒有傳遞幫助 flag,我們把相應的參數賦值給合適的變量。 ### 編寫邏輯 每個人寫代碼的方式各有不同,不過一般錯誤處理都是我們最后會思考的事情。這對程序整體的設計并不好,不過它對快速原型有幫助。因為 Rust 強制我們進行顯示的錯誤處理(通過讓我們調用`unwrap`),這樣很容易看出我們的程序的那一部分可以造成錯誤。 在這個案例學習中,邏輯真的很簡單。所有我們要做的就是解析給我們的 CSV 數據并打印出匹配的行的一個字段。讓我們開始吧。(確保在你的文件開頭加上`extern crate csv;`。) ~~~ // 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. #[derive(Debug, RustcDecodable)] 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 print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <data-path> <city>", program))); } fn main() { let args: Vec<String> = env::args().collect(); let program = args[0].clone(); let mut opts = Options::new(); opts.optflag("h", "help", "Show this usage message."); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(e) => { panic!(e.to_string()) } }; if matches.opt_present("h") { print_usage(&program, opts); return; } let data_file = args[1].clone(); let data_path = Path::new(&data_file); let city = args[2].clone(); let file = fs::File::open(data_path).unwrap(); let mut rdr = csv::Reader::from_reader(file); for row in rdr.decode::<Row>() { let row = row.unwrap(); if row.city == city { println!("{}, {}: {:?}", row.city, row.country, row.population.expect("population count")); } } } ~~~ 讓我們概括下錯誤。我們可以從明顯的開始:三個`unwrap`被調用的地方: 1. [`File::open`](http://doc.rust-lang.org/std/fs/struct.File.html#method.open)可能返回[`io::Error`](http://doc.rust-lang.org/std/io/struct.Error.html)。 1. [`csv::Reader::decode`](http://burntsushi.net/rustdoc/csv/struct.Reader.html#method.decode)一次解碼一行,而且[解碼一個記錄](http://burntsushi.net/rustdoc/csv/struct.DecodedRecords.html)(查看`Iterator`實現的關聯類型`Item`)可能產生一個[`csv::Error`](http://burntsushi.net/rustdoc/csv/enum.Error.html)。 1. 如果`row.population`是`None`,那么調用`expect`會 panic。 還有其他的嗎?如果我們無法找到一個匹配的城市呢?想`grep`這樣的工具會返回一個錯誤碼,所以可能我們也應該這么做。所以我們有特定于我們的問題,IO 錯誤和 CSV 解析錯誤的邏輯錯誤。我們將探索兩個不同方式來處理這個問題。 我像從`Box<Error>`開始。接著,我們看看如何定義有用的自定義錯誤類型。 ### 使用`Box<Error>`處理錯誤 `Box<Error>`的好處是它剛剛夠用。你并不需要定義你自己的錯誤類型而且也不需要任何`From`實現。缺點是因為`Box<Error>`是一個 trait 對象,這意味著編譯器無法再推導出底層類型。 [之前](#)我們開始了把我們函數類型從`T`變成`Result<T, OurErrorType>`的重構。在這個例子中,`OurErrorType`就是`Box<Error>`。不過`T`是什么?或者我們可以給`main`添加一個返回類型嗎? 第二個問題的答案是不行,我們不能這么做。這意味著我們需要寫一個新函數。不過`T`是什么?最簡單的辦法是返回一個作為`Vec<Row>`的匹配上的`Row`的值。(更好的代碼會返回一個迭代器,不過這是一個留給讀者的練習。) 讓我們重構函數,不過保持對`unwrap`的調用。注意我們選擇處理一個不存在的人口數行的方式是單純的忽略它。 ~~~ 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 print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <data-path> <city>", program))); } 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: Vec<String> = env::args().collect(); let program = args[0].clone(); let mut opts = Options::new(); opts.optflag("h", "help", "Show this usage message."); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(e) => { panic!(e.to_string()) } }; if matches.opt_present("h") { print_usage(&program, opts); return; } let data_file = args[1].clone(); let data_path = Path::new(&data_file); let city = args[2].clone(); for pop in search(&data_path, &city) { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } } ~~~ 雖然我們去掉了一個`expect`調用(它是一個比`unwrap`要好的變體),我們仍要處理任何不存在的搜索結果。 為了把這轉化為合適的錯誤處理,我們需要做如下事情: 1. 把`search`的返回值類型改為`Result<Vec<PopulationCount>, Box<Error>>`. 1. 使用[`try!`宏]()這樣會返回錯誤給調用者而不是使程序 panic。 1. 處理`mian`中的錯誤。 讓我們試試: ~~~ fn search<P: AsRef<Path>> (file_path: P, city: &str) -> Result<Vec<PopulationCount>, Box<Error+Send+Sync>> { let mut found = vec![]; let file = try!(fs::File::open(file_path)); let mut rdr = csv::Reader::from_reader(file); for row in rdr.decode::<Row>() { let row = try!(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) } } ~~~ 現在我們用`try!(x)`代替了`x.unwrap()`。因為我們的函數返回一個`Result<T, E>`,`try!`宏在出現錯誤時會提早返回。 代碼中還有另一個大的需要注意的地方:我們用了`Box<Error + Send + Sync>`而不是`Box<Error>`。這么做是因為我們可以把一個字符串轉換為一個錯誤類型。我們需要這些額外的 bound,這樣我們就可以使用[相應的`From`實現](http://doc.rust-lang.org/std/convert/trait.From.html)了: ~~~ // 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> ~~~ 因為`search`現在返回`Result<T, E>`,`main`應該在調用`search`時使用 case analysis: ~~~ ... match search(&data_file, &city) { Ok(pops) => { for pop in pops { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } } Err(err) => println!("{}", err) } ... ~~~ 現在你看到了我們如何正確的處理`Box<Error>`,讓我們嘗試一種使用我們自定義錯誤類型的不同方式。不過首先,讓我們先放下錯誤處理并快速的添加從`stdin`讀取的功能。 ### 從標準輸入讀取 在我們的程序中,我們接受一個單文件輸入并進行一次數據解析。這意味著我們可能需要能夠接受標準輸入。不過你可能也喜歡現在的格式——所以讓我們同時擁有兩者吧! 添加標準輸入支持是非常簡單的。我們只必需做三件事: 1. 修改程序參數,這樣一個單獨的參數——城市——可以被接受,同時人口數據從標準輸入讀取。 1. 修改程序,這樣一個`-f`選項可以接受文件,如果它沒有從標準輸入傳遞。 1. 修改`search`函數接受一個可選的文件路徑。當為`None`時,它應該知道從標準輸入讀取。 首先,這是新的使用方法函數: ~~~ fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <city>", program))); } ~~~ 下一部分只會變得稍微難一點: ~~~ ... let mut opts = Options::new(); opts.optopt("f", "file", "Choose an input file, instead of using STDIN.", "NAME"); opts.optflag("h", "help", "Show this usage message."); ... let file = matches.opt_str("f"); let data_file = file.as_ref().map(Path::new); let city = if !matches.free.is_empty() { matches.free[0].clone() } else { print_usage(&program, opts); return; }; for pop in search(&data_file, &city) { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } ... ~~~ 在這段代碼中,我們獲取`file`(它的類型是`Option<String>`),并轉換為一個`search`可用的類型,在這個例子中,是`&Option<AsRef<Path>>`。為此,我們獲取一個文件的引用,并執行映射`Path::new`。在這里,`as_ref()`把`Option<String>`轉換為`Option<&str>`,而且從這開始,我們可以對內容的`Option`執行`Path::new`,并返回新值的`Option`。當一切搞定,這就變為簡單的獲取`city`參數并執行`search`函數了。 修改`search`需要一點技巧。`csv`crate 可以用[任何實現了`io::Read`的類型]()構建一個解析器。不過我們如何對這兩個類型(注:因該是`Option`的兩個值)使用相同的代碼呢?事實上有多種方法可以做到。其中之一是重寫`search`為接受一個滿足`io::Read`的`R`類型參數的泛型。另一個辦法是使用 trait 對象: ~~~ 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(try!(fs::File::open(file_path))), }; let mut rdr = csv::Reader::from_reader(input); // The rest remains unchanged! } ~~~ ### 用自定義類型處理錯誤 之前,我們學習了如何[用自定義錯誤類型組合錯誤](#)。我們定義了一個`enum`的錯誤類型并實現了`Error`和`From`。 因為我們有三個不同的錯誤(IO,CSV 解析和未找到),讓我們定義一個三個變體的`enum`: ~~~ #[derive(Debug)] enum CliError { Io(io::Error), Csv(csv::Error), NotFound, } ~~~ 現在讓我們實現`Display`和`Error`: ~~~ 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", } } } ~~~ 在我們可以在`search`函數中使用`CliError`之前,我們需要提供一系列的`From`實現。我們如何知曉該提供那個實現呢?好吧,我們得把`io::Error`和`csv::Error`都轉換為`CliError`。他們都只是外部錯誤,所以目前我們只需要兩個`From`實現: ~~~ 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) } } ~~~ 因為[`try!`的定義](#)`From`的實現是很重要的。尤其是在這個例子中,如果出現錯誤,錯誤的`From::from`被調用,將被轉換為我們的錯誤類型`CliError`。 當實現了`From`,我們只需要對`search`函數進行兩個小的修改:返回值類型和“未找到”錯誤。這是全部的代碼: ~~~ 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(try!(fs::File::open(file_path))), }; let mut rdr = csv::Reader::from_reader(input); for row in rdr.decode::<Row>() { let row = try!(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) } } ~~~ 不再需要其他的修改。 ### 增加功能 編寫泛型代碼是很好的,因為泛用性是很酷的,并且之后會變得很有用。不過有時并不值得這么做。看看我們上一部分我們是怎么做的: 1. 定義了一個新的錯誤類型。2.增加`Error`,`Display`和兩個`From`實現。 這里最大的缺點是我們的程序并沒有改進多少。這里仍然有很多用`enum`代表錯誤的額外操作,特別是在這樣短小的程序里。 像我們這樣使用自定義錯誤類型的一個有用的方面是`main`函數現在可以選擇不同的處理錯誤的方式。之前使用`Box<Error>`的時候并沒有什么選擇:只能打印信息。我們現在仍可以這么做,不過只是在我們想這么做的時候,例如,添加一個`--quiet` flag?`--quiet` flag 應該能夠消除任何冗余的輸出。 現在如果程序不能匹配一個城市,它會打印一個信息說它不能。這可能有點蠢,尤其是你想要你的程序能在 shell 腳本中使用的時候。 所以讓我們開始增加 flag。就像之前一樣,我們需要修改用法字符串,并給選項變量添加 flag。當我們寫完這些,Getopts 會搞定剩下的操作: ~~~ ... let mut opts = Options::new(); opts.optopt("f", "file", "Choose an input file, instead of using STDIN.", "NAME"); opts.optflag("h", "help", "Show this usage message."); opts.optflag("q", "quiet", "Silences errors and warnings."); ... ~~~ 現在我們只需要實現我們的“安靜”功能。這要求我們修改`mian`中的 case analysis: ~~~ match search(&args.arg_data_path, &args.arg_city) { Err(CliError::NotFound) if args.flag_quiet => process::exit(1), Err(err) => panic!("{}", err), Ok(pops) => for pop in pops { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } } ~~~ 當然,在出現 IO 錯誤或者數據解析失敗時我們并不想變得安靜。因此,我們用 case analysis 來檢查錯誤類型是否是`NotFound`以及`--quiet`是否被啟用。如果,搜索失敗了,我們仍然使用一個錯誤碼退出(使用`grep`的傳統)。 如果我們還在用`Box<Error>`,那么實現`--quiet`功能將變得很復雜。 我們的案例學習講了很多東西。從這時起,你應該能夠在現實生活中編寫帶有合適錯誤處理的程序和庫了。 ### 精簡版 因為這個章節很長,有一個 Rust 錯誤處理的快速總結是很有幫助的。有很多好的“拇指規則”。需要強調的是他們*并非*教條。這里每一個建議都可能有適當的理由予以反駁! - 如果你在寫小的事例代碼這時錯誤處理顯得負擔過重,可能使用`unwrap`([`Result::unwrap`](http://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap),[`Option::unwrap`](http://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap),或是更可取的[`Option::expect`](http://doc.rust-lang.org/std/option/enum.Option.html#method.expect))是足夠的。你的代碼的客戶應該知道如何正確的處理錯誤。(如果他們并不知道,教會他們吧!) - 如果你在 hack(quick 'n' dirty)程序,不要為你使用`unwrap`而感羞愧。不過你被警告過了:如果別人踩到了坑,不要因為他們對糟糕的錯誤信息火冒三丈而感到驚訝! - 如果你在 hack 程序并對 panic 感到羞愧,那么使用`String`或者`Box<Error + Send + Sync>`作為你的錯誤類型(選擇`Box<Error + Send + Sync>`是因為它有[可用的`From`實現](http://doc.rust-lang.org/std/convert/trait.From.html))。 - 否則,在程序中,定義你自己的錯誤類型并實現合適的[`From`](http://doc.rust-lang.org/std/convert/trait.From.html)和[`Error`](http://doc.rust-lang.org/std/error/trait.Error.html)來讓[`try!`](http://doc.rust-lang.org/std/macro.try!.html)宏變得更工程化。 - 如果你在寫一個庫并且它可能產生錯誤,定義你自己的錯誤類型并實現[`std::error::Error`](http://doc.rust-lang.org/std/error/trait.Error.html) trait。如果可以的話,實現[`From`](http://doc.rust-lang.org/std/convert/trait.From.html)來讓你的庫代碼和調用者的代碼更加容易編寫。(因為 Rust 的一致性規則,調用者不能為你的錯誤類型實現`From`,所以你的庫應該實現。) - 學習定義于[`Option`](http://doc.rust-lang.org/std/option/enum.Option.html)和[`Result`](http://doc.rust-lang.org/std/result/enum.Result.html)中的組合。只使用他們有時可能比較累人,不過我個人發現合理的結合`try!`和組合是比較誘人的。`and_then`,`map`和`unwrap_or`是我們的最愛。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看