<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
程式在執行的過程中,總是會不可避免地產生錯誤,而如何優雅地解決錯誤,也是語言的設計哲學之一。那麼現有的主流語言是怎麼處理錯誤的呢?比如呼叫一個函數,如果函數執行的時候出錯了,那麼該怎麼處理呢。
C 語言
C 是一門古老的語言,通常會以指標作為引數,在函數內部進行解除參照,修改指標指向的值。然後用 1 和 0 代表返回值,如果返回 1,則表示修改成功;返回 0,表示修改失敗。
但這種做法有一個缺陷,就是修改失敗時,無法將原因記錄下來。
C++ 和 Python
引入了 Exception,通過 try catch 可以將異常捕獲,相比 C 進步了一些。但它的缺陷是我們不知道被呼叫方會丟擲什麼異常。
Java
引入了 checked exception,方法的所有者可以宣告自己會丟擲什麼異常,然後呼叫者對異常進行處理。在 Java 程式啟動時,丟擲大量異常都是司空見慣的事情,並在相應的呼叫堆疊中將資訊完整地記錄下來。至此,Java 的異常不再是異常,而是一種很普遍的結構,從良性到災難性都有所使用,異常的嚴重性由呼叫者來決定。
而像 Go、Rust 這樣的新興語言,則採用了與之不同的方式。它們沒有像傳統的高階語言一樣引入 try cache,因為設計者認為這會把控制流搞得非常亂。在 Go 和 Rust 裡面,錯誤是通過返回值體現的。
比如開啟一個檔案,如果檔案不存在,像 Python 程式就會直接報錯。但 Go 不一樣,Go 在開啟檔案的時候會同時返回一個檔案控制程式碼和 error,如果檔案成功開啟,那麼 error 就是空;如果檔案開啟失敗,那麼 error 就是錯誤原因。
所以對於 Go 而言,在可能出錯的時候,程式會同時返回 value 和 error。如果你要使用 value,那麼必須先對 error 進行判斷。
我們上面提到了錯誤(Error)和異常(Exception),有很多人分不清這兩者的區別,我們來解釋一下。
在 Python 裡面很少會對錯誤和異常進行區分,甚至將它們視做同一種概念。但在 Go 和 Rust 裡面,錯誤和異常是完全不同的,異常要比錯誤嚴重得多。
當出現錯誤時,開發者是有能力解決的,比如檔案不存在。這時候程式並不會有異常產生,而是正常執行,只是作為返回值的 error 不為空,開發者要基於 error 進行下一步處理。
但如果出現了異常,那麼一定是程式碼寫錯了,開發者無法處理了。比如索引越界,程式會直接 panic 掉,所以在 Rust 裡面異常又叫做不可恢復的錯誤。
如果在 Rust 裡面出現了異常,也就是不可恢復的錯誤,那麼就表示開發者希望程式立刻中止掉,不要再執行下去了。
而不可恢復的錯誤,除了程式在執行過程中因為某些原因自然產生之外,也可以手動引發。
fn main() { println!("程式開始執行"); // 在 Go 裡面引發異常通過 panic 函數 // Rust 則是通過 panic! 宏,還是挺相似的 panic!("發生了不可恢復的錯誤"); println!("程式不會執行到這裡"); }
注意 panic! 和 println! 的引數一致的,都支援字串格式化輸出。下面看一下輸出結果:
如果將環境變數 RUST_BACKTRACE 設定為 1,還可以顯示呼叫棧。
然後除了 panic! 之外,assert 系列的宏也可以生成不可恢復的錯誤。
fn main() { // 如果 assert! 裡面的布林值為真,無事發生 // 如果為假,那麼程式會 panic 掉 assert!(1 == 2); // assert!(1 == 2) 還可以寫成 assert_eq!(1, 2); // 除了 assert_eq! 外,還有 assert_ne! assert_ne!(1, 2); // 不過最常用的還是 assert! }
還有一個宏叫 unimplemented!,當我們的程式碼還沒有開發完畢時,為了在別人呼叫的時候能夠提示呼叫者,便可以使用這個宏。
fn get_data() { unimplemented!("還沒開發完畢,by {}", "古明地覺"); } fn main() { get_data() }
它和 Python 裡的 raise NotImplementedError 是比較相似的。
最後在 Rust 裡面還有一個常用的宏,用於表示程式不可能執行到某個地方。
fn divide_by_3(n: u32) -> u32 { // 找到可以滿足 3 * i 大於 n 的最小整數 i for i in 0 .. { if 3 * i > n { return i; } } // 顯然程式不可能執行到這裡 // 因為 for 迴圈是無限進行的,最終一定會 return // 但 Rust 在編譯時,從語法上是判斷不出來的 // 它只知道這個函數目前不完整,因為如果 for 迴圈結束, // 那麼返回值就不符合 u32 型別了,儘管我們知道 for 迴圈不可能結束 // 為此我們可以隨便 return 一個 u32,並寫上註釋 // "此處是為了保證函數簽名合法,但程式不會執行到這裡" // 而更專業的做法是使用一個宏 unreachable!("程式不可能執行到這裡"); }
如果程式真的執行到了該宏所在的地方,那麼同樣會觸發一個不可恢復的錯誤。
以上就是 Rust 裡面的幾個用於建立不可恢復的錯誤的幾個宏。
說完了不可恢復的錯誤,再來看看可恢復的錯誤,一般稱之為錯誤。在 Go 裡面錯誤是通過多返回值實現的,如果程式可能出現錯誤,那麼會多返回一個 error,然後根據 error 是否為空來判斷究竟有沒有產生錯誤。所以開發者必須先對 error 進行處理,然後才可以執行下一步,不應該對 error 進行假設。
而 Rust 的錯誤機制和 Go 類似,只不過是通過列舉實現的,該列舉叫 Result,我們看一下它的定義。
pub enum Result<T, E> { Ok(T), Err(E), }
如果將定義簡化一下,那麼就是這個樣子。可以看到它就是一個簡單的列舉,並且帶有兩個泛型。我們之前也介紹過一個列舉叫 Option,用來處理空值的,內部有兩個成員,分別是 Some 和 None。
然後列舉 Result 和 Option 一樣,它和內部的成員都是可以直接拿來用的,我們實際舉個例子演示一下吧。
// 計算兩個 i32 的商 fn divide(a: i32, b: i32) -> Result<i32, &'static str> { let ret: Result<i32, &'static str>; // 如果 b != 0,返回 Ok(a / b) if b != 0 { ret = Ok(a / b); } else { // 否則返回除零錯誤 ret = Err("ZeroDivisionError: division by zero") } return ret; } fn main() { let a = divide(100, 20); println!("a = {:?}", a); let b = divide(100, 0); println!("b = {:?}", b); /* a = Ok(5) b = Err("ZeroDivisionError: division by zero") */ }
列印結果如我們所料,但 Rust 和 Go 一樣,都要求我們提前對 error 進行處理,並且 Rust 比 Go 更加嚴格。對於 Go 而言,在沒有發生錯誤的時候,即使我們不對 error 做處理(不推薦),也是沒問題的。而 Rust 不管會不會發生錯誤,都要求對 error 進行處理。
因為 Rust 返回的是列舉,比如上面程式碼中的 a 是一個 Ok(i32),即便沒有發生錯誤,這個 a 也不能直接用,必須使用 match 表示式處理一下。
fn main() { // 將返回值和 5 相加,由於 a 是 Ok(i32) // 顯然它不能直接和 i32 相加 let a = divide(100, 20); match a { Ok(i) => println!("a + 5 = {}", i + 5), Err(error) => println!("出錯啦: {}", error), } let b = divide(100, 0); match b { Ok(i) => println!("b + 5 = {}", i + 5), Err(error) => println!("出錯啦: {}", error), } /* a + 5 = 10 出錯啦: ZeroDivisionError: division by zero */ }
雖然這種編碼方式會讓人感到有點麻煩,但它杜絕了出現執行時錯誤的可能。相比執行時報錯,我們寧可在編譯階段多費些功夫。
我們說 Rust 為了避免控制流混亂,並沒有引入 try cache 語句。但 try cache 也有它的好處,就是可以完整地記錄堆疊資訊,從錯誤的根因到出錯的地方,都能完整地記錄下來,舉個 Python 的例子:
程式報錯了,根因是呼叫了函數 f,而出錯的地方是在第 10 行,我們手動 raise 了一個異常。可以看到程式將整個錯誤的鏈路全部記錄下來了,只要從根因開始一層層往下定位,就能找到錯誤原因。
而對於 Go 和 Rust 來說就不方便了,特別是 Go,如果每返回一個 error,就列印一次,那麼會將 error 打的亂七八糟的。所以我們更傾向於錯誤能夠在上下文當中傳遞,對於 Rust 而言,我們可以通過問號表示式來實現這一點。
fn external_some_func() -> Result<u32, &'static str> { // 外部的某個函數 Ok(666) } fn call1() -> Result<f64, &'static str> { // 我們要呼叫 external_some_func match external_some_func() { // 型別轉化在 Rust 裡面通過 as 關鍵字 Ok(i) => Ok((i + 1) as f64), Err(error) => Err(error) } } // 但是上面這種呼叫方式有點繁瑣 // 我們還可以使用問號表示式 fn call2() -> Result<f64, &'static str> { // 注:使用問號表示式有一個前提 // 呼叫方和被呼叫方的返回值都要是 Result 列舉型別 // 並且它們的錯誤型別要相同,比如這裡都是 &'static str let ret = external_some_func()?; Ok((ret + 1) as f64) } fn main() { println!("{:?}", call1()); // Ok(667.0) println!("{:?}", call2()); // Ok(667.0) }
裡面的 call1 和 call2 是等價的,如果在 call2 裡面函數呼叫出錯了,那麼會自動將錯誤返回。並且注意 call2 裡面的 ret,它是 u32,不是 Ok(u32)。因為函數呼叫出錯會直接返回,不出錯則會將 Ok 裡面的 u32 取出來賦值給 ret。
然後我們說如果 external_some_func 函數執行出錯了,那麼 call2 就直接將錯誤返回了,程式不會再往下執行。所以這也側面要求,call2 和 external_some_func 的返回值型別都是 Result,並且裡面的錯誤型別也要一樣,否則函數簽名是不合法的。
fn external_some_func() -> Result<u32, &'static str> { // 外部的某個函數 Err("函數執行出錯") } fn call1() -> Result<f64, &'static str> { match external_some_func() { Ok(i) => Ok((i + 1) as f64), Err(error) => Err(error) } } fn call2() -> Result<f64, &'static str> { let ret = external_some_func()?; Ok((ret + 1) as f64) } fn main() { println!("{:?}", call1()); // Err("函數執行出錯") println!("{:?}", call2()); // Err("函數執行出錯") }
此時錯誤就自動地在上下文當中傳遞了,並且還更簡潔,只需要在函數呼叫後面加一個問號即可。
再來考慮一種更復雜的情況,我們在呼叫函數的時候可能會呼叫多個函數,而這多個函數的錯誤型別不一樣該怎麼辦呢?
struct FileNotFoundError { err: String, filename: String, } struct IndexError { err: &'static str, index: u32, } fn external_some_func1() -> Result<u32, FileNotFoundError> { Err(FileNotFoundError { err: String::from("檔案不存在"), filename: String::from("main.py"), }) } fn external_some_func2() -> Result<i32, IndexError> { Err(IndexError { err: "索引越界了", index: 9, }) }
很多時候,錯誤並不是一個簡單的字串,因為那樣能攜帶的資訊太少。基本上都是一個結構體,文字格式的錯誤資訊只是裡面的欄位之一,而其它欄位則負責描述更加詳細的上下文資訊。
我們上面有兩個函數,是一會兒我們要呼叫的,但問題是它們返回的錯誤型別不同,也就是 Result<T, E> 裡面的 E 不同。而如果是這種情況的話,問號表示式就會失效,那麼我們應該怎麼做呢?
// 其它程式碼不變 #[derive(Debug)] enum MyError { Error1(FileNotFoundError), Error2(IndexError) } // 為 MyError 實現 From trait // 分別是 From<FileNotFoundError> 和 From<IndexError> impl From<FileNotFoundError> for MyError { fn from(error: FileNotFoundError) -> MyError { MyError::Error1(error) } } impl From<IndexError> for MyError { fn from(error: IndexError) -> MyError { MyError::Error2(error) } } fn call1() -> Result<i32, MyError>{ // 呼叫的兩個函數、和當前函數返回的錯誤型別都不相同 // 但是當前函數是合法的,因為 MyError 實現了 From trait // 當錯誤型別是 FileNotFoundError 或 IndexError 時 // 它們會呼叫 MyError 實現的 from 方法 // 然後將錯誤統一轉換為 MyError 型別 let x = external_some_func1()?; let y = external_some_func2()?; Ok(x as i32 + y) } fn call2() -> Result<i32, MyError>{ let y = external_some_func2()?; let x = external_some_func1()?; Ok(x as i32 + y) } fn main() { println!("{:?}", call1()); /* Err(Error1(FileNotFoundError { err: "檔案不存在", filename: "main.py" })) */ println!("{:?}", call2()); /* Err(Error2(IndexError { err: "索引越界了", index: 9 })) */ }
如果呼叫的多個函數返回的錯誤型別相同,那麼只需要保證呼叫方也返回相同的錯誤型別,即可使用問號表示式。但如果呼叫的多個函數返回的錯誤型別不同,那麼這個時候呼叫方就必須使用一個新的錯誤型別,其資料結構通常為列舉。
而列舉裡的成員要包含所有可能發生的錯誤型別,比如這裡的FileNotFoundError和IndexError。然後為列舉實現 From trait,該 trait 帶了一個泛型,並且內部定義了一個 from 方法。
我們在實現之後,當出現 FileNotFoundError 和 IndexError 的時候,就會呼叫 from 方法,轉成呼叫方的 MyError 型別,然後返回。
因此這就是 Rust 處理錯誤的方式,可能有一些難理解,需要私下多琢磨琢磨。最後再補充一點,我們知道 main 函數應該返回一個空元組,但除了空元組之外,它也可以返回一個 Result。
fn main() -> Result<(), MyError> { // 如果 call1() 的後面沒有加問號 // 那麼在呼叫沒有出錯的時候,返回的就是 Ok(...) // 呼叫出錯的時候,返回的就是 Err(...) // 但不管哪一種,都是 Result<T, E> 型別 println!("{:?}", call1()); // 如果加了 ? 那麼就不一樣了 // 在呼叫沒出錯的時候,會直接將 Ok(...) 裡面的值取出來 // 呼叫出錯的時候,當前函數會中止執行, // 並將被呼叫方(這裡是 call2)的錯誤作為呼叫方(這裡是 main)的返回值返回 // 此時通過問號表示式,就實現了錯誤在上下文當中傳遞 // 所以這也要求被呼叫方返回的錯誤型別要和呼叫方相同 println!("{:?}", call2()?); // 為了使函數簽名合法,這裡要返回一個值,直接返回 Ok(()) 即可 // 但上面的 call2()? 是會報錯的,所以它下面的程式碼都不會執行 Ok(()) }
我們執行一下看看輸出:
由於 main 函數已經是最頂層的呼叫方了,所以出錯的時候,直接將錯誤丟擲來了。
以上就是 Rust 的錯誤處理,相比其它語言來說,確實難理解了一些。另外從該系列的開始到現在,我們介紹的都屬於基礎內容,而且有些地方介紹的還不夠詳細,後續我們會將這些內容以更深入的方式做一個補充。
到此這篇關於一文帶你瞭解Rust是如何處理錯誤的的文章就介紹到這了,更多相關Rust處理錯誤內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45