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

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

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

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

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

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

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

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

