Rust入坑指南:亡羊補牢
如果你已經開始學習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程式一定會變得更加健壯