探索?Rust?中實用的錯誤處理技巧
錯誤是軟件中不可否認(rèn)的事實,所以 Rust 有一些處理出錯情況的特性。在許多情況下,Rust 要求你承認(rèn)錯誤的可能性,并在你的代碼編譯前采取一些行動。這一要求使你的程序更加健壯,因為它可以確保你在將代碼部署到生產(chǎn)環(huán)境之前就能發(fā)現(xiàn)錯誤并進(jìn)行適當(dāng)?shù)奶幚怼?/p>
Rust 將錯誤分為兩大類:可恢復(fù)的(recoverable)和 不可恢復(fù)的(unrecoverable)錯誤。對于一個可恢復(fù)的錯誤,比如文件未找到的錯誤,我們很可能只想向用戶報告問題并重試操作。不可恢復(fù)的錯誤總是 bug 出現(xiàn)的征兆,比如試圖訪問一個超過數(shù)組末端的位置,因此我們要立即停止程序。
大多數(shù)語言并不區(qū)分這兩種錯誤,并采用類似異常這樣方式統(tǒng)一處理它們。Rust 沒有異常。相反,它有 Result<T, E> 類型,用于處理可恢復(fù)的錯誤,還有 panic! 宏,在程序遇到不可恢復(fù)的錯誤時停止執(zhí)行。本章首先介紹 panic! 調(diào)用,接著會講到如何返回 Result<T, E>。此外,我們將探討在決定是嘗試從錯誤中恢復(fù)還是停止執(zhí)行時的注意事項。
1、用 panic! 處理不可恢復(fù)的錯誤
突然有一天,代碼出問題了,而你對此束手無策。對于這種情況,Rust 有 panic!宏。在實踐中有兩種方法造成 panic:執(zhí)行會造成代碼 panic 的操作(比如訪問超過數(shù)組結(jié)尾的內(nèi)容)或者顯式調(diào)用 panic! 宏。這兩種情況都會使程序 panic。通常情況下這些 panic 會打印出一個錯誤信息,展開并清理棧數(shù)據(jù),然后退出。通過一個環(huán)境變量,你也可以讓 Rust 在 panic 發(fā)生時打印調(diào)用堆棧(call stack)以便于定位 panic 的原因。
對應(yīng) panic 時的棧展開或終止當(dāng)出現(xiàn) panic 時,程序默認(rèn)會開始 展開(unwinding),這意味著 Rust 會回溯棧并清理它遇到的每一個函數(shù)的數(shù)據(jù),不過這個回溯并清理的過程有很多工作。另一種選擇是直接 終止(abort),這會不清理數(shù)據(jù)就退出程序。
那么程序所使用的內(nèi)存需要由操作系統(tǒng)來清理。如果你需要項目的最終二進(jìn)制文件越小越好,panic 時通過在 Cargo.toml 的
[profile]部分增加panic = 'abort',可以由展開切換為終止。例如,如果你想要在 release 模式中 panic 時直接終止:
[profile.release] panic = 'abort'
我們可以再程序中主動拋出一個錯誤,如下圖所示:
fn main() {
panic!("error error error ...")
}運行一下程序,會打印如下信息:

通過上圖可以知道:
第一行顯示的是程序代碼發(fā)生錯誤的位置,main.rs 的第二行第五列開始的。
第二行顯示的是panic!里面,我們自定義的錯誤內(nèi)容。
第三行告訴我們可以使用 panic! 被調(diào)用的函數(shù)的 backtrace 來尋找代碼中出問題的地方。
1.1 使用 panic! 的 backtrace
讓我們來看看另一個因為我們代碼中的 bug 引起的別的庫中 panic! 的例子,而不是直接的宏調(diào)用。示例如下所示:
fn main() {
let arr = [10, 20, 30, 40, 50];
arr[100];
}這里嘗試訪問 vector 的第一百個元素(這里的索引是 99 因為索引從 0 開始),不過它只有三個元素。這種情況下 Rust 會 panic。[] 應(yīng)當(dāng)返回一個元素,不過如果傳遞了一個無效索引,就沒有可供 Rust 返回的正確的元素。
C 語言中,嘗試讀取數(shù)據(jù)結(jié)構(gòu)之后的值是未定義行為(undefined behavior)。你會得到任何對應(yīng)數(shù)據(jù)結(jié)構(gòu)中這個元素的內(nèi)存位置的值,甚至是這些內(nèi)存并不屬于這個數(shù)據(jù)結(jié)構(gòu)的情況。這被稱為 緩沖區(qū)溢出(buffer overread),并可能會導(dǎo)致安全漏洞,比如攻擊者可以像這樣操作索引來讀取儲存在數(shù)據(jù)結(jié)構(gòu)之后不被允許的數(shù)據(jù)。
為了保護(hù)程序遠(yuǎn)離這類漏洞,如果嘗試讀取一個索引不存在的元素,Rust 會停止執(zhí)行并拒絕繼續(xù)。嘗試運行上面的程序會出現(xiàn)如下:

報錯:運行時遇到panic錯誤,在main.rs第三行第五列開始,索引超過邊界,長度為5,而索引值確實100。
讓我們將 RUST_BACKTRACE 環(huán)境變量設(shè)置為1 的值來獲取 backtrace 看看。

這是數(shù)組,報錯比較簡單,如果其他數(shù)據(jù)結(jié)構(gòu)我們可以看一下結(jié)果,例如:string
fn main() {
let arr = String::from("hello");
arr[100];
}打印結(jié)果如下所示:

這里有大量的輸出!你實際看到的輸出可能因不同的操作系統(tǒng)和 Rust 版本而有所不同。為了獲取帶有這些信息的 backtrace,必須啟用 debug 標(biāo)識。當(dāng)不使用 --release 參數(shù)運行 cargo build 或 cargo run 時 debug 標(biāo)識會默認(rèn)啟用,就像這里一樣。
在上圖中,我們可以看到報錯具體的文件以及對應(yīng)的行號,下面還有rust程序報錯更加詳細(xì)的原因,這樣可以更快的為我們解決問題,提升自己的效率。
2、用 Result 處理可恢復(fù)的錯誤
大部分錯誤并沒有嚴(yán)重到需要程序完全停止執(zhí)行。有時候,一個函數(shù)失敗,僅僅就是因為一個容易理解和響應(yīng)的原因。例如,如果因為打開一個并不存在的文件而失敗,此時我們可能想要創(chuàng)建這個文件,而不是終止進(jìn)程。
Result 枚舉,它定義有如下兩個成員,Ok 和 Err:
enum Result<T, E> {
Ok(T),
Err(E),
}T 和 E 是泛型類型參數(shù);現(xiàn)在你需要知道的就是 T 代表成功時返回的 Ok 成員中的數(shù)據(jù)的類型,而 E 代表失敗時返回的 Err 成員中的錯誤的類型。因為 Result 有這些泛型類型參數(shù),我們可以將 Result 類型和標(biāo)準(zhǔn)庫中為其定義的函數(shù)用于很多不同的場景,這些情況中需要返回的成功值和失敗值可能會各不相同。
讓我們調(diào)用一個返回 Result 的函數(shù),因為它可能會失?。嚎匆幌氯缦率纠?/p>
use std::fs::File;
fn main() {
let file_result = File::open("hello.txt");
}File::open 的返回值是 Result<T, E>。泛型參數(shù) T 會被 File::open 的實現(xiàn)放入成功返回值的類型 std::fs::File,這是一個文件句柄。錯誤返回值使用的 E 的類型是 std::io::Error。這些返回類型意味著 File::open 調(diào)用可能成功并返回一個可以讀寫的文件句柄。這個函數(shù)調(diào)用也可能會失敗:例如,也許文件不存在,或者可能沒有權(quán)限訪問這個文件。File::open 函數(shù)需要一個方法在告訴我們成功與否的同時返回文件句柄或者錯誤信息。這些信息正好是 Result 枚舉所代表的。
當(dāng) File::open 成功時,greeting_file_result 變量將會是一個包含文件句柄的 Ok 實例。當(dāng)失敗時,greeting_file_result 變量將會是一個包含了更多關(guān)于發(fā)生了何種錯誤的信息的 Err 實例。
這里使用match表達(dá)式來處理結(jié)果:
fn main() {
let file_result = File::open("hello.txt");
let res = match file_result {
Ok(file) => file,
Err(err) => panic!("打開文件發(fā)生錯誤...{:?}", err),
};
}當(dāng)我們運行以上代碼時,看一下輸出結(jié)果如何:

2.1 匹配不同的錯誤
上面的代碼不管 File::open 是因為什么原因失敗都會 panic!。我們真正希望的是對不同的錯誤原因采取不同的行為:如果 File::open 因為文件不存在而失敗,我們希望創(chuàng)建這個文件并返回新文件的句柄。如果 File::open 因為任何其他原因失敗,例如沒有打開文件的權(quán)限,我們可以通過不同分支把錯誤提示的更加詳細(xì)。
fn main() {
let file_result = File::open("hello.txt");
match file_result {
Ok(file) => file,
Err(err) => match err.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(file) => file,
Err(err) => panic!("創(chuàng)建文件失敗 {:?}", err),
},
other_error => panic!("其他錯誤 {:?}", other_error),
},
};
}File::open 返回的 Err 成員中的值類型 io::Error,它是一個標(biāo)準(zhǔn)庫中提供的結(jié)構(gòu)體。這個結(jié)構(gòu)體有一個返回 io::ErrorKind 值的 kind 方法可供調(diào)用。io::ErrorKind 是一個標(biāo)準(zhǔn)庫提供的枚舉,它的成員對應(yīng) io 操作可能導(dǎo)致的不同錯誤類型。我們感興趣的成員是 ErrorKind::NotFound,它代表嘗試打開的文件并不存在。這樣,match 就匹配完 greeting_file_result 了,不過對于 error.kind() 還有一個內(nèi)層 match。
我們希望在內(nèi)層 match 中檢查的條件是 error.kind() 的返回值是否為 ErrorKind的 NotFound 成員。如果是,則嘗試通過 File::create 創(chuàng)建文件。然而因為 File::create 也可能會失敗,還需要增加一個內(nèi)層 match 語句。當(dāng)文件不能被打開,會打印出一個不同的錯誤信息。外層 match 的最后一個分支保持不變,這樣對任何除了文件不存在的錯誤會使程序 panic。
2.2 失敗時 panic 的簡寫:unwrap 和 expect
match 能夠勝任它的工作,不過它可能有點冗長并且不總是能很好的表明其意圖。Result<T, E> 類型定義了很多輔助方法來處理各種情況。其中之一叫做 unwrap,它的實現(xiàn)就像上個示例中的 match 語句。如果 Result 值是成員 Ok,unwrap 會返回 Ok 中的值。如果 Result 是成員 Err,unwrap 會為我們調(diào)用 panic!。這里是一個實踐 unwrap 的例子:
fn main() {
let file_result = File::open("hello.txt").unwrap();
}運行一下這個程序,看下對應(yīng)的輸出:

還有另一個類似于 unwrap 的方法它還允許我們選擇 panic! 的錯誤信息:expect。使用 expect 而不是 unwrap 并提供一個好的錯誤信息可以表明你的意圖并更易于追蹤 panic 的根源。expect 的語法看起來像這樣:
fn main() {
let file_result = File::open("hello.txt").expect("沒有讀取到文件");
}expect 與 unwrap 的使用方式一樣:返回文件句柄或調(diào)用 panic! 宏。expect 在調(diào)用 panic! 時使用的錯誤信息將是我們傳遞給 expect 的參數(shù),而不像 unwrap 那樣使用默認(rèn)的 panic! 信息。它看起來像這樣:

在生產(chǎn)級別的代碼中,大部分 Rustaceans 選擇 expect 而不是 unwrap 并提供更多關(guān)于為何操作期望是一直成功的上下文。
2.3 傳播錯誤
當(dāng)編寫一個其實先會調(diào)用一些可能會失敗的操作的函數(shù)時,除了在這個函數(shù)中處理錯誤外,還可以選擇讓調(diào)用者知道這個錯誤并決定該如何處理。這被稱為 傳播(propagating)錯誤,這樣能更好的控制代碼調(diào)用,因為比起你代碼所擁有的上下文,調(diào)用者可能擁有更多信息或邏輯來決定應(yīng)該如何處理錯誤。
例如,示例 9-6 展示了一個從文件中讀取用戶名的函數(shù)。如果文件不存在或不能讀取,這個函數(shù)會將這些錯誤返回給調(diào)用它的代碼:
fn main() {
fn read_file() -> Result<String, io::Error> {
let file_result = File::open("hello.txt");
let v = String::from("open file success ...");
match file_result {
Ok(_) => Ok(v),
Err(err) => Err(err),
}
}
let res = read_file();
print!("{:?}", res)
}這個函數(shù)可以編寫成更加簡短的形式,不過我們以大量手動處理開始以便探索錯誤處理;在最后我們會展示更短的形式。讓我們看看函數(shù)的返回值:Result<String, io::Error>。這意味著函數(shù)返回一個 Result<T, E> 類型的值,其中泛型參數(shù) T 的具體類型是 String,而 E 的具體類型是 io::Error。
如果這個函數(shù)沒有出任何錯誤成功返回,函數(shù)的調(diào)用者會收到一個包含 String 的 Ok 值 —— 函數(shù)從文件中讀取到的用戶名。如果函數(shù)遇到任何錯誤,函數(shù)的調(diào)用者會收到一個 Err 值,它儲存了一個包含更多這個問題相關(guān)信息的 io::Error 實例。
到此這篇關(guān)于探索 Rust 中實用的錯誤處理技巧的文章就介紹到這了,更多相關(guān)Rust 錯誤處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Rust使用csv crate構(gòu)建CSV文件讀取器的全過程
這篇文章主要學(xué)習(xí)如何基于Rust使用csv這個crate構(gòu)建一個CSV文件讀取器的過程,學(xué)習(xí)了csv相關(guān)的用法以及一些往期學(xué)過的crate的復(fù)習(xí),兼顧了實用性和Rust的學(xué)習(xí),需要的朋友可以參考下2024-05-05

