一文詳解Rust中的錯誤處理
本文略有刪減,原文請訪問錯誤處理。
panic! 宏代表一個程序無法處理的狀態(tài),并停止執(zhí)行而不是使用無效或不正確的值繼續(xù)處理。
Rust 類型系統(tǒng)的 Result 枚舉代表操作可能會在一種可以恢復(fù)的情況下失敗,可以使用 Result 來告訴代碼調(diào)用者他需要處理潛在的成功或失敗。
用 panic! 處理不可恢復(fù)的錯誤
在實踐中有兩種方法造成 panic:執(zhí)行會造成代碼 panic 的操作(比如訪問超過數(shù)組結(jié)尾的內(nèi)容)或者顯式調(diào)用 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'
在一個簡單的程序中調(diào)用 panic!:
fn main() { panic!("crash and burn"); }
使用 panic! 的 backtrace
讓我們來看看另一個因為我們代碼中的 bug 引起的別的庫中 panic! 的例子,而不是直接的宏調(diào)用。
嘗試訪問超越 vector 結(jié)尾的元素,這會造成 panic!:
fn main() { let v = vec![1, 2, 3]; v[99]; }
可以設(shè)置 RUST_BACKTRACE 環(huán)境變量來得到一個 backtrace,backtrace 是一個執(zhí)行到目前位置所有被調(diào)用的函數(shù)的列表。Rust 的 backtrace 跟其他語言中的一樣:閱讀 backtrace 的關(guān)鍵是從頭開始讀直到發(fā)現(xiàn)你編寫的文件。
將 RUST_BACKTRACE 環(huán)境變量設(shè)置為任何不是 0 的值來獲取 backtrace 看看:
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
2: core::panicking::panic_bounds_check
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
6: panic::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
為了獲取帶有這些信息的 backtrace,必須啟用 debug 標(biāo)識。當(dāng)不使用 --release 參數(shù)運(yùn)行 cargo build 或 cargo run 時 debug 標(biāo)識會默認(rèn)啟用,就像這里一樣。
Windows設(shè)置 RUST_BACKTRACE 環(huán)境變量的兩種方式
在cmd中執(zhí)行
set RUST_BACKTRACE=1
在powershell中執(zhí)行:
$env:RUST_BACKTRACE=1 ; cargo run
用 Result 處理可恢復(fù)的錯誤
使用 Result 類型來處理潛在的錯誤中的那個 Result 枚舉,它定義有Ok 和 Err兩個成員:
enum Result<T, E> { Ok(T), Err(E), }
T 和 E 是泛型類型參數(shù):
- T 代表成功時返回的 Ok 成員中的數(shù)據(jù)的類型
- E 代表失敗時返回的 Err 成員中的錯誤的類型
調(diào)用一個返回 Result 的函數(shù),它打開一個文件,可能會失?。?/p>
use std::fs::File; fn main() { let greeting_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)用可能成功并返回一個可以讀寫的文件句柄,也可能會失敗,如文件不存在或無訪問權(quán)限。
需要在代碼中增加根據(jù) File::open 返回值進(jìn)行不同處理的邏輯:
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; }
如果當(dāng)前目錄沒有一個叫做 hello.txt 的文件,當(dāng)運(yùn)行這段代碼時 panic! 宏的輸出能告訴了我們出錯的地方。
匹配不同的錯誤
使用不同的方式處理不同類型的錯誤:
use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => match error.kind() { //嘗試打開的文件并不存在,通過 File::create 創(chuàng)建文件 ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(e) => panic!("Problem creating the file: {:?}", e), }, other_error => { panic!("Problem opening the file: {:?}", other_error); } }, }; }
File::open
返回的 Err 成員中的值類型io::Error
,它是一個標(biāo)準(zhǔn)庫中提供的結(jié)構(gòu)體。io::Error
結(jié)構(gòu)體有一個返回io::ErrorKind
值的kind
方法可供調(diào)用。io::ErrorKind
是一個標(biāo)準(zhǔn)庫提供的枚舉,它的成員對應(yīng) io 操作可能導(dǎo)致的不同錯誤類型。
不同于使用 match 和 Result<T, E>
在處理代碼中的 Result<T, E> 值時,相比于使用 match ,使用閉包和 unwrap_or_else 方法會更加簡潔:
use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {:?}", error); }) } else { panic!("Problem opening the file: {:?}", error); } }); }
失敗時 panic 的簡寫:unwrap 和 expect
Result<T, E> 類型定義了很多輔助方法來處理各種情況,其中之一叫做 unwrap:
- 如果 Result 值是成員 Ok,unwrap 會返回 Ok 中的值。
- 如果 Result 是成員 Err,unwrap 會為我們調(diào)用 panic!。
一個實踐 unwrap 的例子:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
另一個類似于 unwrap 的方法它還允許選擇 panic! 的錯誤信息:expect,語法如下:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
expect 與 unwrap 的使用方式一樣:返回文件句柄或調(diào)用 panic! 宏。
在生產(chǎn)級別的代碼中,大部分人選擇 expect 而不是 unwrap 并提供更多關(guān)于為何操作期望是一直成功的上下文。
傳播錯誤
一個從文件中讀取用戶名的函數(shù),如果文件不存在或不能讀取,這個函數(shù)會將這些錯誤返回給調(diào)用它的代碼:
use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), //最后一個表達(dá)式,無需顯式調(diào)用 return 語句 } }
函數(shù)的返回值類型為 Result<String, io::Error>,說明函數(shù)返回一個 Result<T, E> 類型的值:泛型參數(shù) T 的具體類型是 String,而 E 的具體類型是 io::Error。
這里選擇 io::Error
作為函數(shù)的返回值是因為它正好是函數(shù)體中那兩個可能會失敗的操作的錯誤返回值:File::open
函數(shù)和 read_to_string
方法。
傳播錯誤的簡寫:? 運(yùn)算符
一個使用 ? 運(yùn)算符向調(diào)用者返回錯誤的函數(shù):
use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) }
Result 值之后的 ? 被定義為與前面示例中定義的處理 Result 值的 match 表達(dá)式有著完全相同的工作方式。不同的是,? 運(yùn)算符所使用的錯誤值被傳遞給了 from 函數(shù),它定義于標(biāo)準(zhǔn)庫的 From trait 中,其用來將錯誤從一種類型轉(zhuǎn)換為另一種類型。
問號運(yùn)算符之后的鏈?zhǔn)椒椒ㄕ{(diào)用:
use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) }
使用 fs::read_to_string 而不是打開后讀取文件
use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") }
哪里可以使用 ? 運(yùn)算符
? 運(yùn)算符只能被用于返回值與 ? 作用的值相兼容的函數(shù),因為 ? 運(yùn)算符被定義為從函數(shù)中提早返回一個值,函數(shù)的返回值必須是 Result 才能與這個 return 相兼容。
嘗試在返回 () 的 main 函數(shù)中使用 ? 的代碼不能編譯:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt")?; }
當(dāng)編譯這些代碼時會出錯,錯誤指出只能在返回 Result 或者其它實現(xiàn)了 FromResidual 的類型的函數(shù)中使用 ? 運(yùn)算符。
為了修復(fù)這個錯誤,有兩個選擇。一個是,如果沒有限制的話將函數(shù)的返回值改為 Result<T, E>。另一個是使用 match 或 Result<T, E> 的方法中合適的一個來處理 Result<T, E>。
在 Option<T> 上調(diào)用 ? 運(yùn)算符的行為與 Result<T, E> 類似:如果值是 None,此時 None 會從函數(shù)中提前返回。如果值是 Some,Some 中的值作為表達(dá)式的返回值同時函數(shù)繼續(xù)。
在 Option 值上使用 ? 運(yùn)算符:
//從給定文本中返回第一行最后一個字符 fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() }
注意你可以在返回 Result 的函數(shù)中對 Result 使用 ? 運(yùn)算符,可以在返回 Option 的函數(shù)中對 Option 使用 ? 運(yùn)算符,但是不可以混合搭配。? 運(yùn)算符不會自動將 Result 轉(zhuǎn)化為 Option,反之亦然;在這些情況下,可以使用類似 Result 的 ok 方法或者 Option 的 ok_or 方法來顯式轉(zhuǎn)換。
修改 main 返回 Result<(), E> 允許對 Result 值使用 ? 運(yùn)算符:
use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; Ok(()) }
目前可以將 Box<dyn Error> 理解為 “任何類型的錯誤”,在返回 Box<dyn Error> 錯誤類型 main 函數(shù)中對 Result 使用 ? 是允許的,因為它允許任何 Err 值提前返回。
要不要 panic!
示例、代碼原型和測試都非常適合 panic
調(diào)用一個類似 unwrap 這樣可能 panic! 的方法可以被理解為一個你實際希望程序處理錯誤方式的占位符,它根據(jù)其余代碼運(yùn)行方式可能會各不相同。
在我們準(zhǔn)備好決定如何處理錯誤之前,unwrap和expect方法在原型設(shè)計時非常方便。當(dāng)我們準(zhǔn)備好讓程序更加健壯時,它們會在代碼中留下清晰的標(biāo)記。
當(dāng)我們比編譯器知道更多的情況
當(dāng)你有一些其他的邏輯來確保 Result 會是 Ok 值時,調(diào)用 unwrap 或者 expect 也是合適的,雖然編譯器無法理解這種邏輯:
use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid");
可以看出 127.0.0.1 是一個有效的 IP 地址,所以這里使用 expect 是可以接受的。
錯誤處理指導(dǎo)原則
在當(dāng)有可能會導(dǎo)致有害狀態(tài)的情況下建議使用 panic!,有害狀態(tài)是指當(dāng)一些假設(shè)、保證、協(xié)議或不可變性被打破的狀態(tài)(如無效的值、自相矛盾的值或者被傳遞了不存在的值),外加如下幾種情況:
- 有害狀態(tài)是非預(yù)期的行為,與偶爾會發(fā)生的行為相對,比如用戶輸入了錯誤格式的數(shù)據(jù)。
- 在此之后代碼的運(yùn)行依賴于不處于這種有害狀態(tài),而不是在每一步都檢查是否有問題。
- 沒有可行的手段來將有害狀態(tài)信息編碼進(jìn)所使用的類型中的情況。
當(dāng)接收到無效輸入時,優(yōu)先返回錯誤信息以通知用戶。若繼續(xù)執(zhí)行可能引發(fā)安全問題或嚴(yán)重后果,則調(diào)用 panic! 停止程序并指出bug。同樣,在遇到無法修復(fù)的外部代碼無效狀態(tài)時,適宜使用 panic!。
當(dāng)錯誤屬于預(yù)期范圍(如解析錯誤或HTTP限流響應(yīng)),應(yīng)返回 Result,表明可能出現(xiàn)失敗,并將問題傳遞給調(diào)用者處理。此時不宜使用 panic! 來應(yīng)對這些情況。
當(dāng)代碼執(zhí)行操作可能因無效值而危及安全時,應(yīng)先驗證其有效性,并在無效時 panic!。這是因為嘗試處理此類數(shù)據(jù)易暴露漏洞,如數(shù)組越界訪問會導(dǎo)致 panic! 以防止?jié)撛诘陌踩L(fēng)險。函數(shù)遵循輸入條件的契約,若違反契約則通過 panic! 指出調(diào)用方 bug,因為這種錯誤通常無法在函數(shù)內(nèi)部妥善處理,需要程序員修復(fù)源代碼。函數(shù)契約及其可能導(dǎo)致 panic! 的情況應(yīng)在 API 文檔中明確說明。
雖然大量錯誤檢查可能冗長繁瑣,但 Rust 的類型系統(tǒng)和編譯器能幫你自動進(jìn)行許多檢查。若函數(shù)參數(shù)為非 Option 類型,編譯器確保其非空且有有效值,因此無需在代碼中處理 Some/None 情況。傳遞空值的代碼無法通過編譯,故函數(shù)運(yùn)行時無須判空。同樣,使用如 u32 的無符號整型可確保值永不會為負(fù)數(shù)。
創(chuàng)建自定義類型進(jìn)行有效性驗證
使用 if 表達(dá)式檢查值是否超出范圍(代碼冗余、可能影響性能):
loop { // --snip-- let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; if guess < 1 || guess > 100 { println!("The secret number will be between 1 and 100."); continue; } match guess.cmp(&secret_number) { // --snip-- }
可以創(chuàng)建一個新類型來將驗證放入創(chuàng)建其實例的函數(shù)中,而不是到處重復(fù)這些檢查。這樣就可以安全地在函數(shù)簽名中使用新類型并相信它們接收到的值。
一個 Guess 類型,它只在值位于 1 和 100 之間時才繼續(xù):
pub struct Guess { //私有的字段,確保了不會存在沒有通過 Guess::new 函數(shù)的條件檢查的 value value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } //有時被稱為 getter,目的就是返回對應(yīng)字段的數(shù)據(jù) pub fn value(&self) -> i32 { self.value } }
以上就是一文詳解Rust中的錯誤處理的詳細(xì)內(nèi)容,更多關(guān)于Rust錯誤處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Rust實現(xiàn)一個表達(dá)式Parser小結(jié)
這篇文章主要為大家介紹了Rust實現(xiàn)一個表達(dá)式Parser小結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Rust?連接?PostgreSQL?數(shù)據(jù)庫的詳細(xì)過程
這篇文章主要介紹了Rust?連接?PostgreSQL?數(shù)據(jù)庫的完整代碼,本文圖文實例代碼相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-01-01