編寫程序,錯誤處理是不可避免的。
但程序員總是偏向正常的情況,而容易忽略有錯誤的情況。
返回值錯誤處理
最開始,錯誤是通過返回值來表示,比如非零表示錯誤,0表示成功。而處理錯誤的代碼類似
這樣的代碼,錯誤處理代碼和業(yè)務邏輯交織在一起,也容易忽略處理錯誤。以及把返回值只用于錯誤返回,有點浪費的感覺。因為很多時候把計算結果作為返回值,更符合思考的邏輯。
異常錯誤處理
后面出現(xiàn)了異常的方式,在出錯的時候,拋出異常。異常一層一層往上拋,如果沒有處理異常,那么程序就會被terminate. 比如C++和Java采用這種方式。
使用異常的代碼類似
看起來錯誤處理代碼與業(yè)務邏輯分開,比較清晰。但有如下的不足,
錯誤處理也容易被忽略,不寫相應的catch,
上層調用者在嵌套很多層的時候,很難知道底層是否會拋異常。
這么寫代碼,在catch的地方,分不清楚是在哪里出了錯誤,step1?step3?
注:python把異常還用于程序控制流改變,如StopInteractionException用于跳出循環(huán)。
Java里異常還分checked exception和unchecked exception。checked exception是必須要處理的異常,從而可以避免被忽略。但checked exception有其局限性,比如添加新的checked exception,會改變接口簽名,變得不能向前兼容。
綜上,我們需要一種錯誤處理
避免無意識地忽略。
可閱讀性強。
其中返回值和異常都可能會被無意識忽略??勺x性,異常好于返回值,且避免占用了返回值。而不可忽略的Java checked exception有它自己的問題。
就沒有其他更好的方式了嗎?Rust給出了它的答案,使用Result 類型。
什么是Result和類型?
Result的完整形態(tài)是Result
一個是沒有錯誤時的計算結果
一個是出錯時,要返回的錯誤
第一點,我們可以看到,現(xiàn)在返回值可以用于返回函數(shù)計算的結果了,沒有被錯誤占領。
第二點,因為返回的值又不是計算結果,所以程序員不能直接使用返回值,需要先檢查具體的類型,沒有出錯時,才能使用計算結果。這樣又避免了無意識的忽略錯誤。
我們可以簡陋地認為Result類型,是C++里面的tag union,即包含一個tag的union。其中tag是錯誤標記,如果是0表示成功,非零表示錯誤,而union則存放著具體的錯誤或者具體的計算結果。(很多時候Result,稱作是和類型 sum type)
可以避免無意識地忽略錯誤,那么可讀性呢?
因為返回值不是計算結果,需要檢查一下才能繼續(xù)下一步,這不就跟錯誤返回值一樣了嗎?
注:先把話說明,沒有錯誤處理的代碼是可讀性最好的。因為只有happy path,第一步,第二步等等。但我們討論在可能出錯的時候的可讀性。
Result和類型的代碼可以是
哇咔咔,這看上去可讀性很差那。實話說,這么寫的代碼的確沒有什么可讀性。
但Rust提供了另外一個寫法,如下
let res = step1()?;let res= step2()?;let res = step3()?;
這個寫法看起來很像異常的情況。業(yè)務邏輯和錯誤處理沒有交織在一起。
眼尖的讀者會發(fā)現(xiàn)每個函數(shù)都有個問號?。而錯誤處理就藏在?后面。
問號的存在,讓Rust自動幫你檢查返回值,在出錯的時候直接返回錯誤,不再繼續(xù)往下走了。問號可以展開為如下的形式(簡化版本,方便理解,實際版本請看官方文檔),
到這里,我們可以看到Rust的創(chuàng)新點在于將錯誤與計算結果放在了返回值,而不是單純地返回錯誤,或者返回計算結果和從第三個路徑返回異常。并且提供了問號和組合子來簡寫錯誤處理。所以同時提供了避免無意識忽略錯誤和提供可讀性。
但錯誤處理遠遠不止這點內容。在我寫了GitHub的webhook微服務 https://github.com/Celthi/github-webhook-gateway 以后,我發(fā)現(xiàn)寫了一大坨下面的代碼
寫成這樣,說明我對Rust的錯誤處理仍然沒有理解到位,于是我試著重構這段代碼,并提了個問題How reduce the nested if and indents?
經(jīng)過重構以后,我發(fā)現(xiàn)了如下的一些情況
有時候只想處理成功的情況,我稱之為“最大努力做事”。所以代碼邏輯是這樣
這也是我自己代碼那么多縮進的原因。它可以通過如下方式來改善,
方式一、首先先把代碼段提到一個單獨的函數(shù)post_sending_task(),然后將返回值改成Result,所以調用的地方代碼是
let _ = best_delivery(); //這里使用使用_,說明我們不關心失敗的情況
在這個best_delivery()里面,我們就可以使用問號表達式了。
方式二、使用組合子,如將Option轉換成Result,從而可以使用問號,如
let res = get_something().ok_or_else(|| err)?;
這里ok_or_else是option上的組合子。什么是組合子,簡單理解是將東西組合在一起的函數(shù)。至于”子“,一種稱謂罷了,要說相似的話,第一反應類似套接字里面的”字“的功能。
方式三、提前返回。通過反轉if的條件,提前返回,比如,
提前返回沒有問號那么可讀性強,但是減少了縮進的層數(shù)。
方式四、如果獲取結果的同時必須處理錯誤的情況,那么使用下面的形式,
注意,問號表達式是適合于獲取結果且不處理錯誤,直接往上拋。
經(jīng)過這四個個方式的改善,我的代碼可讀性提高了,變成了
錯誤處理與日志、錯誤報告
錯誤處理的時候,通常要寫日志。但是錯誤處理和日志是兩碼事。不是所有的錯誤處理都要寫日志,而且不同的錯誤,寫到的日志級別是不一樣的,如調試,信息,錯誤,嚴重等等級別。
錯誤處理是處理出錯的情況,而日志是記錄感興趣的信息。它們有重合,但是關注點不一樣。以后再寫文章。
錯誤報告(error report)跟錯誤處理也是兩碼事,雖然經(jīng)常關聯(lián)在一起,也留作以后再寫文章。
審核編輯:劉清
-
JAVA
+關注
關注
19文章
2952瀏覽量
104479 -
python
+關注
關注
55文章
4767瀏覽量
84375 -
Rust
+關注
關注
1文章
228瀏覽量
6542
原文標題:Rust代碼啟發(fā)之錯誤處理
文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論