探索?Rust?中實(shí)用的錯(cuò)誤處理技巧
錯(cuò)誤是軟件中不可否認(rèn)的事實(shí),所以 Rust 有一些處理出錯(cuò)情況的特性。在許多情況下,Rust 要求你承認(rèn)錯(cuò)誤的可能性,并在你的代碼編譯前采取一些行動。這一要求使你的程序更加健壯,因?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'
我們可以再程序中主動拋出一個(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í)我們可能想要創(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)用也可能會失?。豪纾苍S文件不存在,或者可能沒有權(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ù)可以編寫成更加簡短的形式,不過我們以大量手動處理開始以便探索錯(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