Rust入坑指南:亡羊補牢

如果你已經開始學習Rust,相信你已經體會過Rust編譯器的強大。它可以幫助你避免程序中的大部分錯誤,但是編譯器也不是萬能的,如果程序寫的不恰當,還是會發生錯誤,讓程序崩潰。所以今天我們就來聊一聊Rust中如何處理程序錯誤,也就是所謂的“亡羊補牢”。

基礎概念

在編程中遇到的非正常情況通??梢苑治啵菏О?、錯誤、異常。

Rust中用兩種方式來消除失敗:強大的類型系統和斷言。

對于類型系統,熟悉Java的同學應該比較清楚。例如我們給一個接收參數為int的函數傳入了字符串類型的變量。這是由編譯器幫我們處理的。

rust07-1

關于斷言,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、Result<T, E>、線程恐慌(Panic)、程序終止(Abort)。接下來我們對這些方法進行詳細介紹。

Option

Option我們在Rust入坑指南:千人千構一文中我們進行過一些介紹,它是一種枚舉類型,主要包括兩種值:Some(T)和None,Rust也是靠它來避免空指針異常的。

在前文中,我們并沒有詳細介紹如何從Option中提取出T,其實最基本的,可以用match來提取。而我也在前文中給你提供了官方文檔的鏈接,不知道你有沒有看。如果還沒來得及看也沒有關系,我把我看到的一些方法分享給你。

這里介紹兩種方法,一種是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中提取值的方法,有時我們會覺得每次處理Option都需要先提取,然后再做相應計算這樣的操作比較麻煩,那么有沒有更加高效的操作呢?答案是肯定的,我從文檔中找到了map和and_then這兩種方法。

其中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,我們再來看另一種錯誤處理方法,它也是一個枚舉類型,叫做Result<T, E>,定義如下:

#[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, ()>。從定義中我們可以看到Result<T, E>有兩個變體:Ok(T)和Err(E)。

Result<T, E>用于處理真正意義上的錯誤,例如,當我們想要打開一個不存在的文件時,或者我們想要將一個非數字的字符串轉換為數字時,都會得到一個Err(E)結果。

Result<T, E>的處理方法和Option類似,都可以使用unwrap和expect方法,也可以使用map和and_then方法,并且用法也都類似,這里就不再贅述了。具體的方法使用細節可以自行查看官方文檔。

這里我們來看一下如何處理不同類型的錯誤。

Rust在std::io??槎ㄒ辶送騁壞拇砦罄嘈虴rror,因此我們在處理時可以分別匹配不同的錯誤類型。

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進行了捕獲,結果如圖所示。

rust07-2

Rust雖然打印了恐慌信息,但是并沒有影響程序的執行,我們的代碼 println!("{}", 1 + 2);可以正常執行。

總結

至此,Rust處理錯誤的方法我們已經基本介紹完了,為什么說是基本介紹完了呢?因為還有一些大佬開發了一些第三方庫來幫助我們更加方便的處理錯誤,其中比較有名的有error-chain和failure,這里就不做過多介紹了。

通過本節的學習,相信你的Rust程序一定會變得更加健壯。

posted @ 2020-01-01 13:45  Jackeyzhe  閱讀(...)  評論(...編輯  收藏