W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
ch09-03-to-panic-or-not-to-panic.md
commit 199ca99926f232ee7f581a917eada4b65ff21754
那么,該如何決定何時應該 panic!
以及何時應該返回 Result
呢?如果代碼 panic,就沒有恢復的可能。你可以選擇對任何錯誤場景都調(diào)用 panic!
,不管是否有可能恢復,不過這樣就是你代替調(diào)用者決定了這是不可恢復的。選擇返回 Result
值的話,就將選擇權交給了調(diào)用者,而不是代替他們做出決定。調(diào)用者可能會選擇以符合他們場景的方式嘗試恢復,或者也可能干脆就認為 Err
是不可恢復的,所以他們也可能會調(diào)用 panic!
并將可恢復的錯誤變成了不可恢復的錯誤。因此返回 Result
是定義可能會失敗的函數(shù)的一個好的默認選擇。
在一些類似示例、原型代碼(prototype code)和測試中, panic 比返回 Result
更為合適,不過他們并不常見。讓我們討論一下為何在示例、代碼原型和測試中,以及那些人們認為不會失敗而編譯器不這么看的情況下, panic 是合適的。章節(jié)最后會總結(jié)一些在庫代碼中如何決定是否要 panic 的通用指導原則。
當你編寫一個示例來展示一些概念時,在擁有健壯的錯誤處理代碼的同時也會使得例子不那么明確。例如,調(diào)用一個類似 unwrap
這樣可能 panic!
的方法可以被理解為一個你實際希望程序處理錯誤方式的占位符,它根據(jù)其余代碼運行方式可能會各不相同。
類似地,在我們準備好決定如何處理錯誤之前,unwrap
和expect
方法在原型設計時非常方便。當我們準備好讓程序更加健壯時,它們會在代碼中留下清晰的標記。
如果方法調(diào)用在測試中失敗了,我們希望這個測試都失敗,即便這個方法并不是需要測試的功能。因為 panic!
會將測試標記為失敗,此時調(diào)用 unwrap
或 expect
是恰當?shù)摹?br>
當你有一些其他的邏輯來確保 Result
會是 Ok
值時,調(diào)用 unwrap
也是合適的,雖然編譯器無法理解這種邏輯。你仍然需要處理一個 Result
值:即使在你的特定情況下邏輯上是不可能的,你所調(diào)用的任何操作仍然有可能失敗。如果通過人工檢查代碼來確保永遠也不會出現(xiàn) Err
值,那么調(diào)用 unwrap
也是完全可以接受的,這里是一個例子:
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();
我們通過解析一個硬編碼的字符來創(chuàng)建一個 IpAddr
實例??梢钥闯?nbsp;127.0.0.1
是一個有效的 IP 地址,所以這里使用 unwrap
是可以接受的。然而,擁有一個硬編碼的有效的字符串也不能改變 parse
方法的返回值類型:它仍然是一個 Result
值,而編譯器仍然會要求我們處理這個 Result
,好像還是有可能出現(xiàn) Err
成員那樣。這是因為編譯器還沒有智能到可以識別出這個字符串總是一個有效的 IP 地址。如果 IP 地址字符串來源于用戶而不是硬編碼進程序中的話,那么就 確實 有失敗的可能性,這時就絕對需要我們以一種更健壯的方式處理 Result
了。
在當有可能會導致有害狀態(tài)的情況下建議使用 panic!
—— 在這里,有害狀態(tài)是指當一些假設、保證、協(xié)議或不可變性被打破的狀態(tài),例如無效的值、自相矛盾的值或者被傳遞了不存在的值 —— 外加如下幾種情況:
如果別人調(diào)用你的代碼并傳遞了一個沒有意義的值,最好的情況也許就是 panic!
并警告使用你的庫的人他的代碼中有 bug 以便他能在開發(fā)時就修復它。類似的,如果你正在調(diào)用不受你控制的外部代碼,并且它返回了一個你無法修復的無效狀態(tài),那么 panic!
往往是合適的。
然而當錯誤預期會出現(xiàn)時,返回 Result
仍要比調(diào)用 panic!
更為合適。這樣的例子包括解析器接收到格式錯誤的數(shù)據(jù),或者 HTTP 請求返回了一個表明觸發(fā)了限流的狀態(tài)。在這些例子中,應該通過返回 Result
來表明失敗預期是可能的,這樣將有害狀態(tài)向上傳播,調(diào)用者就可以決定該如何處理這個問題。使用 panic!
來處理這些情況就不是最好的選擇。
當代碼對值進行操作時,應該首先驗證值是有效的,并在其無效時 panic!
。這主要是出于安全的原因:嘗試操作無效數(shù)據(jù)會暴露代碼漏洞,這就是標準庫在嘗試越界訪問數(shù)組時會 panic!
的主要原因:嘗試訪問不屬于當前數(shù)據(jù)結(jié)構(gòu)的內(nèi)存是一個常見的安全隱患。函數(shù)通常都遵循 契約(contracts):他們的行為只有在輸入滿足特定條件時才能得到保證。當違反契約時 panic 是有道理的,因為這通常代表調(diào)用方的 bug,而且這也不是那種你希望所調(diào)用的代碼必須處理的錯誤。事實上所調(diào)用的代碼也沒有合理的方式來恢復,而是需要調(diào)用方的 程序員 修復其代碼。函數(shù)的契約,尤其是當違反它會造成 panic 的契約,應該在函數(shù)的 API 文檔中得到解釋。
雖然在所有函數(shù)中都擁有許多錯誤檢查是冗長而煩人的。幸運的是,可以利用 Rust 的類型系統(tǒng)(以及編譯器的類型檢查)為你進行很多檢查。如果函數(shù)有一個特定類型的參數(shù),可以在知曉編譯器已經(jīng)確保其擁有一個有效值的前提下進行你的代碼邏輯。例如,如果你使用了一個并不是 Option
的類型,則程序期望它是 有值 的并且不是 空值。你的代碼無需處理 Some
和 None
這兩種情況,它只會有一種情況就是絕對會有一個值。嘗試向函數(shù)傳遞空值的代碼甚至根本不能編譯,所以你的函數(shù)在運行時沒有必要判空。另外一個例子是使用像 u32
這樣的無符號整型,也會確保它永遠不為負。
讓我們使用 Rust 類型系統(tǒng)的思想來進一步確保值的有效性,并嘗試創(chuàng)建一個自定義類型以進行驗證?;貞浺幌碌诙碌牟虏驴从螒颍覀兊拇a要求用戶猜測一個 1 到 100 之間的數(shù)字,在將其與秘密數(shù)字做比較之前我們從未驗證用戶的猜測是位于這兩個數(shù)字之間的,我們只驗證它是否為正。在這種情況下,其影響并不是很嚴重:“Too high” 或 “Too low” 的輸出仍然是正確的。但是這是一個很好的引導用戶得出有效猜測的輔助,例如當用戶猜測一個超出范圍的數(shù)字或者輸入字母時采取不同的行為。
一種實現(xiàn)方式是將猜測解析成 i32
而不僅僅是 u32
,來默許輸入負數(shù),接著檢查數(shù)字是否在范圍內(nèi):
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--
}
if
表達式檢查了值是否超出范圍,告訴用戶出了什么問題,并調(diào)用 continue
開始下一次循環(huán),請求另一個猜測。if
表達式之后,就可以在知道 guess
在 1 到 100 之間的情況下與秘密數(shù)字作比較了。
然而,這并不是一個理想的解決方案:如果讓程序僅僅處理 1 到 100 之間的值是一個絕對需要滿足的要求,而且程序中的很多函數(shù)都有這樣的要求,在每個函數(shù)中都有這樣的檢查將是非常冗余的(并可能潛在的影響性能)。
相反我們可以創(chuàng)建一個新類型來將驗證放入創(chuàng)建其實例的函數(shù)中,而不是到處重復這些檢查。這樣就可以安全的在函數(shù)簽名中使用新類型并相信他們接收到的值。示例 9-13 中展示了一個定義 Guess
類型的方法,只有在 new
函數(shù)接收到 1 到 100 之間的值時才會創(chuàng)建 Guess
的實例:
pub struct Guess {
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 }
}
pub fn value(&self) -> i32 {
self.value
}
}
示例 9-13:一個 Guess
類型,它只在值位于 1 和 100 之間時才繼續(xù)
首先,我們定義了一個包含 i32
類型字段 value
的結(jié)構(gòu)體 Guess
。這里是儲存猜測值的地方。
接著在 Guess
上實現(xiàn)了一個叫做 new
的關聯(lián)函數(shù)來創(chuàng)建 Guess
的實例。new
定義為接收一個 i32
類型的參數(shù) value
并返回一個 Guess
。new
函數(shù)中代碼的測試確保了其值是在 1 到 100 之間的。如果 value
沒有通過測試則調(diào)用 panic!
,這會警告調(diào)用這個函數(shù)的程序員有一個需要修改的 bug,因為創(chuàng)建一個 value
超出范圍的 Guess
將會違反 Guess::new
所遵循的契約。Guess::new
會出現(xiàn) panic 的條件應該在其公有 API 文檔中被提及;第十四章會涉及到在 API 文檔中表明 panic!
可能性的相關規(guī)則。如果 value
通過了測試,我們新建一個 Guess
,其字段 value
將被設置為參數(shù) value
的值,接著返回這個 Guess
。
接著,我們實現(xiàn)了一個借用了 self
的方法 value
,它沒有任何其他參數(shù)并返回一個 i32
。這類方法有時被稱為 getter,因為它的目的就是返回對應字段的數(shù)據(jù)。這樣的公有方法是必要的,因為 Guess
結(jié)構(gòu)體的 value
字段是私有的。私有的字段 value
是很重要的,這樣使用 Guess
結(jié)構(gòu)體的代碼將不允許直接設置 value
的值:調(diào)用者 必須 使用 Guess::new
方法來創(chuàng)建一個 Guess
的實例,這就確保了不會存在一個 value
沒有通過 Guess::new
函數(shù)的條件檢查的 Guess
。
于是,一個接收(或返回) 1 到 100 之間數(shù)字的函數(shù)就可以聲明為接收(或返回) Guess
的實例,而不是 i32
,同時其函數(shù)體中也無需進行任何額外的檢查。
Rust 的錯誤處理功能被設計為幫助你編寫更加健壯的代碼。panic!
宏代表一個程序無法處理的狀態(tài),并停止執(zhí)行而不是使用無效或不正確的值繼續(xù)處理。Rust 類型系統(tǒng)的 Result
枚舉代表操作可能會在一種可以恢復的情況下失敗??梢允褂?nbsp;Result
來告訴代碼調(diào)用者他需要處理潛在的成功或失敗。在適當?shù)膱鼍笆褂?nbsp;panic!
和 Result
將會使你的代碼在面對不可避免的錯誤時顯得更加可靠。
現(xiàn)在我們已經(jīng)見識過了標準庫中 Option
和 Result
泛型枚舉的能力了,在下一章讓我們聊聊泛型是如何工作的,以及如何在你的代碼中使用他們。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: