> The best-laid plans of mice and men Often go awry
>
> "Tae a Moose", Robert Burns
>
> 不管是人是鼠,即使最如意的安排設計,結局也往往會出其不意
>
> 《致老鼠》,羅伯特·彭斯
有時,杯具就是發生了。有一個計劃來應對不可避免會發生的問題是很重要的。Rust提供了豐富的處理你軟件中可能(讓我們現實點:將會)出現的錯誤的支持。
主要有兩種類型的錯誤可能出現在你的軟件中:失敗和恐慌。讓我們先看看它們的區別,接著討論下如何處理他們。再接下來,我們討論如何將失敗升級為恐慌。
## 失敗 vs. 恐慌
Rust使用了兩個術語來區別這兩種形式的錯誤:失敗和恐慌。_失敗_(_failure_)是一個可以通過某種方式恢復的錯誤。_恐慌_(_panic_)是不能夠恢復的錯誤。
“恢復”又是什么意思呢?好吧,大部分情況,一個錯誤的可能性是可以預料的。例如,考慮一下`from_str`函數:
~~~
from_str("5");
~~~
這個函數獲取一個字符串參數然后把它轉換為其它類型。不過因為它是一個字符串,你不能夠確定這個轉換是否能成功。例如,這個應該轉壞成什么呢?
~~~
from_str("hello5world");
~~~
這不能工作。所以我們知道這個函數只對一些輸入能夠正常工作。這是我們期望的行為。我們叫這類錯誤為_失敗_。
另一方面,有時,會出現意料之外的錯誤,或者我們不能從中恢復。一個典型的例子是`assert!`:
~~~
assert!(x == 5);
~~~
我們用`assert!`聲明某值為true。如果它不是true,很糟的事情發生了。嚴重到我們不能再當前狀態下繼續執行。另一個例子是使用`unreachable!()`宏:
~~~
enum Event {
NewRelease,
}
fn probability(_: &Event) -> f64 {
// real implementation would be more complex, of course
0.95
}
fn descriptive_probability(event: Event) -> &'static str {
match probability(&event) {
1.00 => "certain",
0.00 => "impossible",
0.00 ... 0.25 => "very unlikely",
0.25 ... 0.50 => "unlikely",
0.50 ... 0.75 => "likely",
0.75 ... 1.00 => "very likely",
}
}
fn main() {
std::io::println(descriptive_probability(NewRelease));
}
~~~
這會給我們一個錯誤:
~~~
error: non-exhaustive patterns: `_` not covered [E0004]
~~~
雖然我們知道我們覆蓋了所有可能的分支,不過Rust不能確定。它不知道概率是在0.0和1.0之間的。所以我們加上另一個分支:
~~~
use Event::NewRelease;
enum Event {
NewRelease,
}
fn probability(_: &Event) -> f64 {
// real implementation would be more complex, of course
0.95
}
fn descriptive_probability(event: Event) -> &'static str {
match probability(&event) {
1.00 => "certain",
0.00 => "impossible",
0.00 ... 0.25 => "very unlikely",
0.25 ... 0.50 => "unlikely",
0.50 ... 0.75 => "likely",
0.75 ... 1.00 => "very likely",
_ => unreachable!()
}
}
fn main() {
println!("{}", descriptive_probability(NewRelease));
}
~~~
我們永遠也不應該觸發`_`分支,所以我們使用`unreachable!()`宏來表明它。`unreachable!()`返回一個不同于`Result`的錯誤。Rust叫這類錯誤為恐慌。
## 使用`Option`和`Result`來處理錯誤
最簡單的表明函數會失敗的方法是使用`Option`類型。還記得我們的`from_str()`例子嗎?這是它的函數標記:
~~~
pub fn from_str<A: FromStr>(s: &str) -> Option<A>
~~~
`from_str()`返回一個`Option`。如果轉換成功了,會返回`Some(value)`,如果失敗了,返回`None`。
這對最簡單的情況是合適的,不過在出錯時并沒有給出足夠的信息。如果我們想知道“為什么”轉換失敗了呢?為此,我們可以使用`Result`類型。它看起來像:
~~~
enum Result<T, E> {
Ok(T),
Err(E)
}
~~~
Rust自身提供了這個枚舉,所以你不需要在你的代碼中定義它。`Ok(T)`變體代表成功,`Err(E)`代表失敗。在所有除了最普通的情況都推薦使用`Result`代替`Option`作為返回值。
這是一個使用`Result`的例子:
~~~
#[derive(Debug)]
enum Version { Version1, Version2 }
#[derive(Debug)]
enum ParseError { InvalidHeaderLength, InvalidVersion }
fn parse_version(header: &[u8]) -> Result<Version, ParseError> {
if header.len() < 1 {
return Err(ParseError::InvalidHeaderLength);
}
match header[0] {
1 => Ok(Version::Version1),
2 => Ok(Version::Version2),
_ => Err(ParseError::InvalidVersion)
}
}
let version = parse_version(&[1, 2, 3, 4]);
match version {
Ok(v) => {
println!("working with version: {:?}", v);
}
Err(e) => {
println!("error parsing header: {:?}", e);
}
}
~~~
這個例子使用了個枚舉,`ParseError`,來列舉各種可能出現的錯誤。
## `panic!`和不可恢復錯誤
當一個錯誤是不可預料的和不可恢復的時候,`panic!`宏會引起一個恐慌。這回使當前線程崩潰,并給出一個錯誤:
~~~
panic!("boom");
~~~
給出:
~~~
thread '<main>' panicked at 'boom', hello.rs:2
~~~
當你運行它的時候。
因為這種情況相對稀少,保守的使用恐慌。
## 升級失敗為恐慌
在特定的情況下,即使一個函數可能失敗,我們也想把它當成恐慌。例如,`io::stdin().read_line()`返回一個`IoResult`,一種形式的`Result`(目前只有Result了,坐等文檔更新),當讀取行出現錯誤時。這允許我們處理和盡可能從錯誤中恢復。
如果你不想處理這個錯誤,或者只是想終止程序,我們可以使用`unwrap()`方法:
~~~
io::stdin().read_line().unwrap();
~~~
如果`Option`是`None`的話`unwrap()`會`panic!`。這基本上就是說“給我一個值,然后如果出錯了的話,直接崩潰。”這與匹配錯誤并嘗試恢復相比更不穩定,不過它的處理明顯更短小。有時,直接崩潰就行。
這是另一個比`unwrap()`稍微聰明點的做法:
~~~
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
~~~
`ok()`將`Result`轉換為`Option`,然后`expect()`做了和`unwrap()`同樣的事,不過帶有一個信息。這個信息會傳遞給底層的`panic!`,當錯誤是這樣可以提供更好的錯誤信息。
## 使用`try!`
當編寫調用那些返回`Result`的函數的代碼時,錯誤處理會是煩人的。`try!`宏在調用棧上隱藏了一些衍生錯誤的樣板。
它可以代替這些:
~~~
use std::fs::File;
use std::io;
use std::io::prelude::*;
struct Info {
name: String,
age: i32,
rating: i32,
}
fn write_info(info: &Info) -> io::Result<()> {
let mut file = File::open("my_best_friends.txt").unwrap();
if let Err(e) = writeln!(&mut file, "name: {}", info.name) {
return Err(e)
}
if let Err(e) = writeln!(&mut file, "age: {}", info.age) {
return Err(e)
}
if let Err(e) = writeln!(&mut file, "rating: {}", info.rating) {
return Err(e)
}
return Ok(());
}
~~~
為下面這些代碼:
~~~
use std::fs::File;
use std::io;
use std::io::prelude::*;
struct Info {
name: String,
age: i32,
rating: i32,
}
fn write_info(info: &Info) -> io::Result<()> {
let mut file = try!(File::open("my_best_friends.txt"));
try!(writeln!(&mut file, "name: {}", info.name));
try!(writeln!(&mut file, "age: {}", info.age));
try!(writeln!(&mut file, "rating: {}", info.rating));
return Ok(());
}
~~~
在`try!`中封裝一個表達式會返回一個未封裝的正確(`Ok`)值,除非結果是`Err`,在這種情況下`Err`會從當前函數提早返回。
值得注意的是你只能在一個返回`Result`的函數中使用`try!`,這意味著你不能在`main()`中使用`try!`,因為`main()`不返回任何東西。
`try!`使用[From](http://doc.rust-lang.org/nightly/std/convert/trait.From.html)特性來確定錯誤時應該返回什么。
- 前言
- 1.介紹
- 2.準備
- 2.1.安裝Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.學習Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的Rust
- 4.高效Rust
- 4.1.棧和堆
- 4.2.測試
- 4.3.條件編譯
- 4.4.文檔
- 4.5.迭代器
- 4.6.并發
- 4.7.錯誤處理
- 4.8.外部語言接口
- 4.9.Borrow 和 AsRef
- 4.10.發布途徑
- 5.語法和語義
- 5.1.變量綁定
- 5.2.函數
- 5.3.原生類型
- 5.4.注釋
- 5.5.If語句
- 5.6.for循環
- 5.7.while循環
- 5.8.所有權
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可變性
- 5.12.結構體
- 5.13.枚舉
- 5.14.匹配
- 5.15.模式
- 5.16.方法語法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait對象
- 5.24.閉包
- 5.25.通用函數調用語法
- 5.26.包裝箱和模塊
- 5.27.`const`和`static`
- 5.28.屬性
- 5.29.`type`別名
- 5.30.類型轉換
- 5.31.關聯類型
- 5.32.不定長類型
- 5.33.運算符和重載
- 5.34.`Deref`強制多態
- 5.35.宏
- 5.36.裸指針
- 6.Rust開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.3.不使用標準庫
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接參數
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 7.詞匯表
- 8.學院派研究
- 勘誤