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