不管是人是鼠,即使最如意的安排設(shè)計,結(jié)局也往往會出其不意。 《致老鼠》 羅伯特·彭斯
有時候,事情會出乎意料的發(fā)生錯誤。重要的是要提前想好應(yīng)對錯誤的方法。Rust 有豐富的支持錯誤處理方法來應(yīng)對可能(老實說:將會)發(fā)生在您的程序中的錯誤。
主要有兩種類型的錯誤可能發(fā)生在你的程序中:故障和異常。讓我們談?wù)剝烧咧g的區(qū)別,然后討論如何處理它們。接著,將討論如何將故障升級為異常。
Rust 使用兩個術(shù)語來區(qū)分兩種形式的錯誤:故障和異常。故障是可以用某種方式中恢復(fù)的錯誤。異常是一種不能恢復(fù)的錯誤。
我們說的 ”恢復(fù)“ 是什么意思?嗯,在大多數(shù)情況下,指的是預(yù)計一個錯誤的可能性。例如,考慮 parse 函數(shù):
"5".parse();
這個方法將一個字符串轉(zhuǎn)換成另一種類型。但因為它是一個字符串,你不能確保轉(zhuǎn)換工作正常執(zhí)行。例如,執(zhí)行如下的轉(zhuǎn)換會得到什么?
"hello5world".parse();
這是行不通的。所以我們知道,這個函數(shù)只會對一些特定的輸入才能正常工作。這是預(yù)期行為。我們稱這種錯誤為故障。
另一方面,有時,有意想不到的錯誤,或者我們不能恢復(fù)它。一個典型的例子是一個斷言:
assert!(x == 5);
我們使用 assert! 說明參數(shù)是正確的。如果這不是正確的,那么這個斷言就是錯誤的。錯誤的話,我們不就能繼續(xù)在當前狀態(tài)往下執(zhí)行了。另一個例子是使用 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]
盡管我們已經(jīng)涵蓋所有我們知道的可能情況情況,但是 Rust 不清楚。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));
}
我們不應(yīng)該得到 _ 情況,所以我們使用 unreachable!() 宏來說明這個。unreachable!() 比結(jié)果給出了不同于 Result 類型的錯誤。Rust 稱這些類型的錯誤為異常。
表明一個函數(shù)可能會失敗的最簡單方法是使用 option< T >類型。例如,字符串 find 方法試圖找到字符串的一個模式串,并返回 Option:
let s = "foo";
assert_eq!(s.find('f'), Some(0));
assert_eq!(s.find('z'), None);
對這些簡單的情況下是可以的,但是在故障的情況下并不會給我們提供很多的信息。如果我們想知道為什么函數(shù)發(fā)生了故障,怎么辦?為此,我們可以使用 Result<T, E>類型。它看起來像這樣:
enum Result<T,E> {
ok(T),
Err(E)
}
這個枚舉類型由 Rust 本身提供,所以你不需要在你的代碼中定義就可以使用它。Ok(T) 變量代表著成功執(zhí)行,Err(E) 變量代表著執(zhí)行失敗。推薦在大多數(shù)情況下返回一個 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);
}
}
這個函數(shù)使用枚舉類型變量 ParseError 列舉各種可能發(fā)生的錯誤。
調(diào)試特點就是讓我們使用 {:?} 格式來打印該枚舉變量的值。
遇到為意料的和不可恢復(fù)的的錯誤時,宏 panic! 會引起異常。這將崩潰當前線程,并給出一個錯誤:
panic!("boom");
當你運行時會輸出:
thred '<main>' panicked at 'boom', hello.rs:2
因為這些類型的情況相對較少見,很少使用恐慌。
在某些情況下,即使一個函數(shù)可能發(fā)生故障,我們?nèi)韵胍阉斪饕粋€異常對待。例如,io::stdin().read_line(&mut buff)
函數(shù)在讀取某一行時出現(xiàn)錯誤會返回 Result< usize >
變量。這使我們能夠處理它并可能從錯誤中恢復(fù)。
如果我們不想處理這個錯誤,而寧愿只是中止程序,那么我們可以使用 unwrap() 方法:
io::stdin().read_line(&mut buffer).unwrap();
如果 Result 變量值是 Err,unwrap() 方法將會產(chǎn)生調(diào)用 panic!,輸出異常。這基本上是說“給我變量的值,如果出現(xiàn)錯誤,就讓程序崩潰?!斑@相對于匹配錯誤并試圖恢復(fù)的方式可靠性較低,但也大大縮短執(zhí)行時間。有時,只是崩潰程序是合理的。
有另一種方式比 unwrap() 方法好一點:
let mut buffer = String::new();
let input = io::stdin().read_line(&mut buffer)
.ok()
.expect("Failed to read line");
ok() 方法將 Result 轉(zhuǎn)換成一個 Option,并 expect() 和 unwrap() 方法做的事是一樣的,不同點在于它需要一個參數(shù),用來輸出提示信息。這個消息被傳遞到底層的 panic!,如果代碼出現(xiàn)錯誤,它提供一個較好的錯誤消息展示方式。
當編寫的代碼調(diào)用很多的返回 Result 類型的函數(shù)時,錯誤處理就變得比較冗長。try! 宏利用堆棧對產(chǎn)生的錯誤進行引用從而隱藏具體的細節(jié)。
將如下的代碼:
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::create("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::create("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! 封裝一個表達式,在成功執(zhí)行時給 Result 賦值為 Ok,否則賦值為 Err,在這種情況下,在函數(shù)未執(zhí)行完成之前就會返回 Err 值。
值得注意的是,你只能在返回 Result 的函數(shù)中使用 try!,這意味著您不能在 main() 中使用 try!,因為 main() 不返回任何值。
try! 利用 From 來確定在錯誤的情況下返回的值。
更多建議: