錯誤處理是程式設計語言中的重要組成部分,是程式開發工作中最重要,也是最容易出問題的地方之一。語言的錯誤處理機制體現了該語言的特點。
錯誤處理主要分為以下幾種
1. 使用全域性錯誤來作為錯誤處理
2. 使用返回值做為錯誤處理
3. 使用異常來做錯誤處理
4. 使用範疇論中的Mond
下面將大概介紹這4種方式,再介紹Rust的錯誤處理的特殊性
1. 使用全域性錯誤來作為錯誤處理
c語言採用了這種方式,此種方式當錯誤發生時,函式呼叫會返回NULL, 錯誤原因會記錄到 全域性變數errno中。
#include <stdio.h>#include <errno.h>#include <string.h>extern int errno ;int main (){ FILE * pf; int errnum; pf = fopen ("unexist.txt", "rb"); if (pf == NULL) { errnum = errno; fprintf(stderr, "errno: %d\n", errno); fprintf(stderr, "open file error: %s\n", strerror( errnum )); } else { fclose (pf); } return 0;}
此種方式的缺點非常明顯,使用全域性變數errno記錄錯誤原因,很容易引起問題。
2. 使用返回值做為錯誤處理
Go 語言採用了此種方式
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } dst, err := os.Create(dstName) if err != nil { return } written, err = io.Copy(dst, src) dst.Close() src.Close() return}
Go需要在每個函式呼叫的地方判斷是否有 err = nil, 這樣的好處是每個函式的錯誤都可以被處理。但缺點也是非常明顯,程式碼中大量
充斥著判斷err的程式碼,導致程式碼非常醜陋,而且可能很多地方基本上不需要關注錯誤,只需要在最頂層處理錯誤,而不是在呼叫鏈上每個地方判斷
3.使用異常
java/c#等採用了這種機制
public float divide(int a, int b) { if(b == 0) { throw new IllegalArgumentException("b can't be 0"); } return a/b;}
這種方式的優點是,只要有異常,上層呼叫如果不關心異常,則可不需要處理,如果關心異常則可以捕獲異常做相應的業務處理,非常的靈活。
缺點是,呼叫者如果不看方法簽名註釋或者原始碼,則不會知道該方法是否會丟擲異常,一旦忘了處理該異常,則有可能會產生bug.
4. 使用範疇論中的Mond
Haskell使用這種方式,該模式使用ADT來封裝錯誤,將錯誤包裹到容器中
divBy :: Integral a => a -> [a] -> Maybe [a]divBy _ [] = Just []divBy _ (0:_) = NothingdivBy numerator (denom:xs) = case divBy numerator xs of Nothing -> Nothing Just results -> Just ((numerator `div` denom) : results)
呼叫方在呼叫divBy函式時,需要對結果進行模式匹配。這種方式是顯式的錯誤處理,呼叫者必選處理錯誤,錯誤被包裹到Mond中,
使用Mond的一序列運算子來處理錯誤。缺點是丟失了原始的錯誤位置資訊,錯誤需要在Mond中處理,需要程式碼架構上對Mond友好。
Rust中的錯誤處理方式
Rust是一門多正規化的語言,錯誤處理吸收了Haskell, Scala的特點。Rust中的錯誤也是包裹到Mond中,不同的是錯誤不需要做模式匹配就可以從Mond中取出,
另外Rust的錯誤處理設計還兼具異常設計的特點,呼叫方如果不關注錯誤,則可以像java中向上將異常冒泡。
Rust錯誤處理的核心是std::result::Result
pub enum Result<T, E> { /// Contains the success value #[lang = "Ok"] #[stable(feature = "rust1", since = "1.0.0")] Ok(#[stable(feature = "rust1", since = "1.0.0")] T), /// Contains the error value #[lang = "Err"] #[stable(feature = "rust1", since = "1.0.0")] Err(#[stable(feature = "rust1", since = "1.0.0")] E),}
Rust中,可以使用模式匹配來處理錯誤,也可以從Result中取出資料或錯誤
fn main(){ let cost = get_cost(true, false); match cost { Ok(price) => println!("price is {}", price), Err(err) => println!("{}", err) }
//或者取出錯誤if cost.is_ok() { println!("price is {}", cost.unwrap());}else { println!("{}", cost.err().unwrap());}
}fn get_cost(num_err:bool, price_err:bool) -> Result<u32, String> { let number = get_number(num_err); match number { Ok(n) => { let price = get_price(price_err); match price { Ok(p) => Ok(n * p), Err(err) => Err(err) } }, Err(err) => Err(err) }}fn get_number(err:bool)-> Result<u32, String> { if !err { Ok(100) } else { Err("Failed to get num".to_string()) }}fn get_price(err:bool) -> Result<u32, String> { if !err { Ok(100) } else { Err("Failed to get price".to_string()) }}
有沒有發現,上面的程式碼比較繁瑣,模式匹配導致容易產生大量的巢狀程式碼,unwrap是一種不夠優雅的方式,如果code review不夠仔細,可能一些unwrap會導致致命異常。
幸運的是Rust提供了try!宏,使用該宏可以讓異常提前返回,效果上類似java異常。rust也支援了try!宏的語法糖?, 大大方便了程式的編寫,以下使用?來重寫上面的例子。
必須注意的是使用try!(?)的關鍵是函式返回值型別必須是std::result::Result<T,E>
fn main() -> Result<(), String> { let cost = get_cost(true, false)?;//這裡如果返回Err,則函式會提前返回 println!("cost is {}", cost); Ok(())}fn get_cost(num_err:bool, price_err:bool) -> Result<u32, String> { let number = get_number(num_err)?;//這裡如果返回Err,則函式會提前返回 let price = get_price(price_err)?;//這裡如果返回Err,則函式會提前返回 Ok(number * price)}fn get_number(err:bool)-> Result<u32, String> { if err { Ok(100) } else { Err("Failed to get price".to_string()) }}fn get_price(err:bool) -> Result<u32, String> { if err { Ok(100) } else { Err("Failed to get price".to_string()) }}
Rust的異常體系中 std::error::Error,是一個核心Trait, 任何實現了該Trait的struct, 標準庫都將自動為其實現From, 這樣該struct就可以轉換為Box<Error + 'a>,如下
impl<'a, E: Error + 'a> From<E> for Box<Error + 'a>
也就是說,如果某個struct, MyError實現了std::error::Error,並且某個方法返回型別為 std::result::Result<T1, MyError>, 且呼叫者返回的型別為
std::result::Result<T1, Box<std::error::Error>>,則呼叫方也可以使用try!宏,還是之前的例子,稍微改動下
#[derive(Debug)]struct MyError{ msg:String}impl Display for MyError{ //實現 Error 必須先實現 Display fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.msg) }}impl Error for MyError{}fn main() -> Result<(), Box<dyn Error>> { let cost = get_cost(true, false)?; println!("cost is {}", cost); Ok(())}fn get_cost(num_err:bool, price_err:bool) -> Result<u32, Box<dyn Error>> { let number = get_number(num_err)?;// MyError實現了 Error trait, 標準庫又為實現了Error trait的struct,實現了impl From for Box<dyn Error>, let price = get_price(price_err)?;// 這樣就可使用try!了 Ok(number * price)}fn get_number(err:bool)-> Result<u32, MyError> { if err { Ok(100) } else { Err(MyError{msg:"Failed to get price".to_string()}) }}fn get_price(err:bool) -> Result<u32, MyError> { if !err { Ok(100) } else { Err(MyError{msg:"Failed to get price".to_string()}) }}
可以看到 std::error:::Error充當了 java/c#中異常基類的作用,任何實現了Error的struct/enum, 方法的返回值只要宣告為Result<T, MyError>,且呼叫者的方法返回值也是Result<T, Box<dyn Error>>,則呼叫者可以使用try!或?語法糖使異常提前返回。
在實戰中,一搬會定義一個業務錯誤類 BussinessError,實現Error trait, 然後所有的方法返回 Result<T, BussinessError>,這樣所有的方法都可以使用?語法糖了。當然當呼叫第三方類庫或者呼叫標準庫時,需要將對應的異常轉為BussinessError。
如果覺得自己定義異常基類比較繁瑣,可以使用第三方類庫,比如anyhow, 該類庫定義了很多異常模式,可以直接拿來使用,以下是使用anyhow改寫後的例子
fn main() -> anyhow::Result<()> { let cost = get_cost(true, false)?; println!("cost is {}", cost); Ok(())}fn get_cost(num_err:bool, price_err:bool) -> anyhow::Result<u32> { let number = get_number(num_err)?; let price = get_price(price_err)?; Ok(number * price)}fn get_number(err:bool)-> anyhow::Result<u32>{ if err { Ok(100) } else { anyhow::bail!("Failed to get price") }}fn get_price(err:bool) -> anyhow::Result<u32> { if !err { Ok(100) } else { anyhow::bail!("Failed to get price") }}
除了anyhow,還有很多其他類庫 如 thiserror, derive-error可以用於異常處理