如果你已經開始學習Rust,相信你已經體會過Rust編譯器的強大。它可以幫助你避免程序中的大部分錯誤,但是編譯器也不是萬能的,如果程序寫的不恰當,還是會發生錯誤,讓程序崩潰。所以今天我們就來聊一聊Rust中如何處理程序錯誤,也就是所謂的“亡羊補牢”。
基礎概念
在編程中遇到的非正常情況通常可以分為三類:失敗、錯誤、異常。
Rust中用兩種方式來消除失敗:強大的類型系統和斷言。
對於類型系統,熟悉Java的同學應該比較清楚。例如我們給一個接收參數為int的函數傳入了字符串類型的變量。這是由編譯器幫我們處理的。
關於斷言,Rust支持6種斷言。分別是:
- assert!
- assert_eq!
- assert_ne!
- debug_assert!
- debug_assert_eq!
- debug_assert_ne!
從名稱我們就可以看出來這6種斷言,可以分為兩大類,帶debug的和不帶debug的,它們的區別就是assert開頭的在調試模式和發布模式下都可以使用,而debug開頭的只可以在調試模式下使用。再來解釋每個大類下的三種斷言,assert!是用於斷言布爾表達式是否為true,assert_eq!用於斷言兩個表達式是否相等,assert_ne!用於斷言兩個表達式是否不相等。當不符合條件時,斷言會引發線程恐慌(panic!)。
Rust處理異常的方法有4種:Option
Option
Option
在前文中,我們並沒有詳細介紹如何從Option
這里介紹兩種方法,一種是expect,另一種是unwrap系列的方法。我們通過一個例子來感受一下。
fn main() {
let a = Some("a");
let b: Option<&str> = None;
assert_eq!(a.expect("a is none"), "a");
assert_eq!(b.expect("b is none"), "b is none"); //匹配到None會引起線程恐慌,打印的錯誤是expect的參數信息
assert_eq!(a.unwrap(), "a"); //如果a是None,則會引起線程恐慌
assert_eq!(b.unwrap_or("b"), "b"); //匹配到None時返回指定值
let k = 10;
assert_eq!(Some(4).unwrap_or_else(|| 2 * k), 4);// 與unwrap_or類似,只不過參數是FnOnce() -> T
assert_eq!(None.unwrap_or_else(|| 2 * k), 20);
}
這是從Option
其中map方法和unwrap一樣,也是一系列方法,包括map、map_or和map_or_else。map會執行參數中閉包的規則,然后將結果再封為Option
fn main() {
let some_str = Some("Hello!");
let some_str_len = some_str.map(|s| s.len());
assert_eq!(some_str_len, Some(6));
}
但是,如果參數本身返回的結果就是Option的話,處理起來就比較麻煩,因為每執行一次map都會多封裝一層,最后的結果有可能是Some(Some(Some(...)))這樣N多層Some的嵌套。這時,我們就可以用and_then來處理了。
利用and_then方法,我們就可以有如下的鏈式調用:
fn main() {
assert_eq!(Some(2).and_then(sq).and_then(sq), Some(16));
}
fn sq(x: u32) -> Option<u32> {
Some(x * x)
}
關於Option
Result<T, E>
聊完了Option
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
實際上,Option
Result<T, E>用於處理真正意義上的錯誤,例如,當我們想要打開一個不存在的文件時,或者我們想要將一個非數字的字符串轉換為數字時,都會得到一個Err(E)結果。
Result<T, E>的處理方法和Option
這里我們來看一下如何處理不同類型的錯誤。
Rust在std::io模塊定義了統一的錯誤類型Error,因此我們在處理時可以分別匹配不同的錯誤類型。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
ErrorKind::PermissionDenied => panic!("Permission Denied!"),
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
在處理Result<T, E>時,我們還有一種處理方法,就是try!宏。它會使代碼變得非常精簡,但是在發生錯誤時,會將錯誤返回,傳播到外部調用函數中,所以我們在使用之前要考慮清楚是否需要傳播錯誤。
對於上面的代碼,使用try!宏就會非常精簡。
use std::fs::File;
fn main() {
let f = try!(File::open("hello.txt"));
}
try!使用起來雖然簡單,但也有一定的問題。像我們剛才提到的傳播錯誤,再就是有可能出現多層嵌套的情況。因此Rust引入了另一個語法糖來代替try!。它就是問號操作符“?”。
use std::fs::File;
use std::io;
use std::io::Read;
fn main() {
read_username_from_file();
}
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
問號操作符必須在處理錯誤的代碼后面,這樣的代碼看起來更加優雅。
恐慌(Panic)
我們從最開始就聊到線程恐慌,那道理什么是恐慌呢?
在Rust中,無法處理的錯誤就會造成線程恐慌,手動執行panic!宏時也會造成恐慌。當程序執行panic!宏時,會打印相應的錯誤信息,同時清理堆棧並退出。但是棧回退和清理會花費大量的時間,如果你想要立即終止程序,可以在Cargo.toml文件中[profile]
區域中增加panic = 'abort'
,這樣當發生恐慌時,程序會直接退出而不清理堆棧,內存空間都由操作系統來進行回收。
程序報錯時,如果你想要查看完整的錯誤棧信息,可以通過設置環境變量 RUST_BACKTRACE=1
的方式來實現。
如果程序發生恐慌,我們前面所說的Result<T, E>就不能使用了,Rust為我們提供了catch_unwind方法來捕獲恐慌。
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {panic!("crash and burn")});
assert!(result.is_err());
println!("{}", 1 + 2);
}
在上面這段代碼中,我們手動執行一個panic宏,正常情況下,程序會在第一行退出,並不會執行后面的代碼。而這里我們用了catch_unwind方法對panic進行了捕獲,結果如圖所示。
Rust雖然打印了恐慌信息,但是並沒有影響程序的執行,我們的代碼 println!("{}", 1 + 2);
可以正常執行。
總結
至此,Rust處理錯誤的方法我們已經基本介紹完了,為什么說是基本介紹完了呢?因為還有一些大佬開發了一些第三方庫來幫助我們更加方便的處理錯誤,其中比較有名的有error-chain和failure,這里就不做過多介紹了。
通過本節的學習,相信你的Rust程序一定會變得更加健壯。