各種編程語言中的「錯誤/異常處理」有哪些成熟的,優雅的或是熱門的機制/思想?
本來很好看的一個函數調用,做一個數據變換,將輸入數據轉換成輸出數據。
dataInput ==&> | function | ==&> dataOutput條理清楚,思路清晰,代碼簡明。但是,在函數的實現過程中難免會出一些問題。而且還是各種方面,各種各樣的。比如輸入的:數據無效,輸入格式錯誤;系統的:內存不足,非法訪問;等等;
錯誤處理使前面簡單好看的線性結構搞得就亂七八糟了,一方面函數的實現要考慮各種異常處理,一方面調用的前後文也要對可能的錯誤進行相應的異常處理。這樣使本來『條理清楚,思路清晰,代碼簡明』的代碼消失/降低了,取代的是各種像『貼滿補丁』一樣的「縝密」的錯誤/異常處理,有時會極大影響代碼的可讀性。然而很多異常,錯誤都是「似曾相識」或是「司空見慣」的,甚至有些異常或結果是否完全可以作為一種『輸出結果』作為處理方式,甚至是否能夠在語言編譯階段能夠枚舉出所有的可能的錯誤和異常情況並在編譯時強制完善?(對程序產生「錯誤/異常」有無成熟的理論分析,得出一些有意義的結論?)感覺上可能會有更優雅的方式來處理這些異常,從而達到各種函數在編寫和調用時程序代碼在結構和邏輯上更加簡潔,縝密,而不是用各種類似於 if( error state1 ) { do something... } else if( error state2 ) { do something... } ...
Rust的異常處理大量借鑒了函數式語言,但採用更便於過程式/面向對象程序員學習的描述方式
enum Option&
Some(T), // Just t |
None // Nothing
}
函數式的equivalent寫在右邊了。相比之下Rust是不是更像C/C++/Java?
當然「像」並沒有什麼卵用,重要的是:在你學Option&fn and_then&(self, f: F) -&> Option&
where F: FnOnce(T) -&> Option&
{
match self{
Some(x) =&> f(x),
None =&> None,
}
}
(文檔在這裡:std::option::Option)
雖然名字逼格很低,但這就是monad的bind。當然所有Rust的教程里都沒有強調這一點,因為Rust並沒有也不需要大面積應用monad(在Rust里使用可變狀態和在C/C++/Java里一樣自然)什麼?return?不會自己賦值啊?我們又不是FP,為什麼一丁點小操作都要定義成一個函數(摔再來看看作為Maybe升級版的Either,Rust里是長這樣的:enum Result&
Ok(T), // Left t |
Err(E) // Right e
}
至少Ok和Err比Left和Right高到不知哪裡去了
標準庫中,可能出錯的函數,多數都會返回Option和Result,不做一些異常處理就不可能得到想要的返回,而且對於Result還用預編譯指令規定了must_use的attribute,如果忽略Result類型的返回值就不能通過編譯,實際上是強迫程序員做錯誤處理。例如從stdin讀一行存進一個字元串是這樣的:use std::io;
let mut s=String::new();
io::stdin().read_line(mut s).ok();
read_line()的作用是往s里寫入讀取到的字元串,並返回一個Result&
use std::io;
let mut s=String::new();
io::stdin().read_line(mut s).
or_else(|e:io::Error|
println!("I/O Error: {}",e);
).ok();
or_else其實就是上面提到的and_then的反義詞,用法不再詳述,看代碼能理解就好
不過仍然有一些函數在一些情況下會panic,而且Rust是沒有catch的,一旦panic整個線程立刻全部掛掉,不可能挽回,要避免這種情況就只能look before you leap。我暫時沒搞清楚Rust這方面的設計哲學實際使用中,儘管標準庫提供了很多像and_then這樣的還算有用的異常處理函數,但多數情況下(包括標準庫自己)稍微複雜一點的一般都會自己寫個match(用法見第二段代碼,和其他函數式語言的match類似,比C系語言的switch更強大)來做,實際上就是條件分支,加上Rust沒有try catch,出錯後無法方便地跳轉,看上去Rust的異常處理是不是簡直和C一樣垃圾?
當然不是,不然我不會寫這篇玩意……實際上Rust是鼓勵你:1、把函數寫短一點,避免出現大量控制分支,亂成一團;2、把異常信息往上傳,只要有一步可能出錯,就應該返回被Option或Result包裹著的值而不是祼的值。
這實際上又是函數式的思想了,當然和函數式相比一個缺點就是無法防止新手寫出爛代碼……即使是用match做異常處理,Rust也有它的優點:Rust規定match必須窮盡所有可能。當然有default(隨便寫一個identifier就表示它可以match任何東西)但是在Rust里,如果你沒有窮盡所有可能又沒有default,就是不合語法的。例如:fn print_is_odd(x:i32){
match x%2{
0 =&> println!("Even");
1 =&> println!("Odd");
_ =&> unreachable!();
}
}
這個match的最後一個分支(等效於default,單獨一個下劃線可以被視為變數名,充當placeholder,而不會引起unused variable的warning)如果省略的話就通不過編譯,即使我們知道邏輯上被match的表達式只可能是0或1,但編譯器不知道,它要求你必須窮盡32位整型的所有可能取值。這裡unreachable!()的效果是直接panic掉並輸出一些錯誤信息,表明你進入了一個邏輯上不可能進入的分支。
總之Rust的match強迫你在語法層面就必須考慮所有可能的異常,當然這仍然無法防止新手用一個ok()或者unwrap()直接丟掉異常……即使是沒有try catch,Rust還是有辦法優雅地跳轉:break和continue可以指定針對哪個循環,當然這不能解決所有跳轉,但至少對於多數C++/Java教程中作為論證try catch的重要性的反例代碼,Rust有辦法利用更靈活的break和continue把它們寫得和try catch差不多優雅
總結Rust的異常處理就是:用函數式慣用的數據結構返回異常,但經常用過程式的慣用法處理,由標準庫做榜樣引導學習者採用這一套方法2016-10-15T13:16+08:00
重要:代碼之髓 (豆瓣) 這本書的第六章看完,就大致可以知道異常處理功能的編年史了。2015-08-31T20:57+08:00
題主提到的:if( error state1 ) {
do something...
} else if( error state2 ) {
do something...
}
實際上就是所謂的 LBYL(Look Before You Leap)。這篇博文 Robust exception handling 這樣描述這種錯誤處理模式:
A common idiom in other languages, sometimes known as "look before you
leap" (LBYL), is to check in advance, before attempting an operation,
for all circumstances that might make the operation invalid.
回想一下自己閱讀過的 C 代碼,相信有不少用 LBYL 方式進行錯誤(異常)處理的吧(調用一個函數後,先根據函數返回值判斷函數是否調用成功。若是,則進行下一步;否,則轉入錯誤處理流程)。
與之對應的,另外一種 EAFP (Easier to Ask for Forgiveness than Permission) ,看起來是這樣的(Python style):try:
do something...
do something else...
...
except ...:
exception(s) handling
嗯。這種代碼執行流程和錯誤處理沒有團的那麼緊,看起來相對也清晰一些。實際上,我貼的第一篇博文和這一篇 Write Cleaner Python: Use Exceptions 都推薦使用 EAFP 來處理 Python 中的異常。
像 C 這種沒有內置異常機制、而且一旦錯誤情況不處理好程序就分分鐘 crash 的語言,慣常以函數返回值來表示異常情況。此種情況下,LBYL 可能就是最好的選擇——如果做的正確的話。此時,無所謂執行流程清晰與否。
而 Python 就不一樣了。異常機制是內置於語言中的,而且很多標準庫模塊都喜歡通過拋出異常來表示程序出錯了。況且,Python 代碼和 OS 之間還有 Python 的虛擬機,我們未處理的異常也不太會導致程序 crash 或「閃退」——你總能看到錯誤提示的。
關於使用 Exception 還是 return code,那就看語言本身有沒有完善的異常處理體系啊。以及,現有的代碼(特別是標準庫的代碼)是以什麼作為 best practice 的。
利益相關:僅學習過 C/C++/Python!把以前看到的一些零零碎碎關於異常處理的東東貼出來吧。
1. Ned Batchelder: Exceptions vs. status returns2. 對使用 C++ 異常處理應具有怎樣的態度? - 編程3. Python 的異常機制及規範是否相當不人性化? - 異常處理4. 為什麼我希望用C而不是C++來實現ZeroMQ 原文:Why should I have written ZeroMQ in C, not C++ (part I)2015-09-01T11:52+08:00
5. 如何優雅的處理(或忽略)c++函數返回值代表的錯誤? - 編程2015-09-02T17:07+08:00
6. Go 語言的錯誤處理機制是一個優秀的設計嗎? - Go 語言2015-09-07T08:50+08:00
7. 知乎上看到一些人評價c++的exception很難用,想問一下大家寫c++時怎麼處理錯誤? - C++
2015-10-10T09:26+08:00
8. 異常(exception)和執行失敗有什麼區別? - Java2015-10-20T20:39+08:00
9. 如何處理C++構造函數中的錯誤——兼談不同語言的錯誤處理10. Rust使用Result的錯誤處理方式與Golang使用error的方式有什麼本質區別? - 程序員2015-11-13T16:11+08:00
C++中編寫強異常安全的代碼真的有必要麼? - 編程2016-03-21T13:39+08:00
如何編寫異常代碼?比如處理bad-alloc的時候,是不是應該先銷毀當前資源? - C++2016-10-18T13:15+08:00
C語言如何不用goto、多處return進行錯誤處理? - Linux----函數式語言的一個常見設計模式就是用monad做錯誤處理。這裡好處在於,錯誤處理的邏輯只要在monad instance那裡實現一遍,然後就不需要在每個call site考慮錯誤處理,減少boilerplate code,提升代碼的模塊性和可讀性。
放一個F#的相關slides:Railway Oriented Programming
以上是最簡單的「在單個monad中實現錯誤處理」,而函數式語言里,monad這個抽象還經常用來封裝各種其他類型的計算,比如帶狀態的計算、非確定性的計算等等,那麼為了進一步提升代碼的模塊性,我們會希望能夠像玩拼圖一樣,用一個monad表達錯誤處理、另一個monad表達帶狀態計算等,然後我的函數需要用到幾個功能,就把幾個monad拼接到一起。這就是monad transformer,比如在Haskell的transformers庫中,提供了ExceptT,可以給任意monad增添一層錯誤處理的功能。學習monad transformers的教程:https://www.cs.virginia.edu/~wh5a/personal/Transformers.pdf
另外,借用Haskell的type class和functional dependencies擴展,我們可以進一步對monad錯誤處理進行抽象,也就是我的代碼甚至不需要關心具體在哪個monad裡面進行錯誤處理。參考Control.Monad.Except中的MonadError這個class。try catch
try { ...} catch (Error0 meow0) {
} catch (Error1 meow1) { } catch (...) { }synchronous signal
進程執行代碼時觸發異常,導致信號的立即遞送,如SIGSEGV、SIGFPE、SIGILL、SIGBUS等。註冊信號處理函數得到通知並進行控制。 在可能觸發信號的地方用sigsetjmp()保存環境,信號處理函數中使用siglongjmp()跳轉到先前保存的環境Resource Acquisition Is Initialization
使用constructor和destructor實現exception-safe resource management 類似的Python with statementC的atexit on_exit
註冊exit()正常退出時需要執行的函數,通常用於釋放資源等。(man 3 atexit) 類似地,Go的defer &error code as return value
C中很常見,錯誤碼作為返回值,或者用專設的errno等變數描述錯誤fork as checkpoint
gdb的checkpoint命令。這個不是很相關,作為錯誤恢復的一個有趣例子bison的error
這個相關性也不大……看在它是compiler compiler的份上stmts:
%empty | stmts "" | stmts exp "
" | stmts error "
"
引入的原義是提供更好的錯誤信息,這給了我們一種啟示:定義異常值和正常值的交互作用
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b 不再把異常視為out-of-band的值,視作運算中能產生的一類特殊值。提供Maybe Either的monad實例來消除語法上的雜訊等。Monad transformer來和其他已有monad複合等λυ-calculus λυt?p?-calculus ... 之前翻到這些東西是為了找實現abstract machine實現delimited control的理論根基……這些還是太艱深了,而且性能不行 $ ext{catch}_alpha M = operatorname{mu}alpha.[alpha]M$ $ ext{throw}_eta M = operatorname{mu}gamma[eta]M ext{ if } gammaotin FV(M)$ Robbert Krebbers. Classical logic, control calculi and data types. Master』s thesis, Radboud University Nijmegen, 2010. 其他絢麗的:multi-prompt delimited control
Let It Crash! The Erlang Approach to Building Reliable Services
其實Erlang也有像Maybe Monad那樣的寫法, 但是沒有chainingJust x &<---&> {:ok, X}Nothing &<---&> {:error, _} 類型可以看作在值上的一個tag, Erlang用比較挫的方法實現而已try...catch...throw
這是目前最常見的異常處理機制了吧。形式類似於try
{
//...
if ( error )
throw xxx;
return;
}
catch condition
{
}
catch condition
{
}
應該是目前最流行和最常見的異常處理機制,函數式有自己的特點,所以形式上有所變化,但本質其實是一樣的。
其實那個catch序列就是模式匹配,,,,,
函數式語言一般有模式匹配的語法,所以寫出來就不一定長這樣,,,至於提問者所說的:
然而很多異常,錯誤都是「似曾相識」或是「司空見慣」的,甚至有些異常或結果是否完全可以作為一種『輸出結果』作為處理方式,甚至是否能夠在語言
編譯階段能夠枚舉出所有的可能的錯誤和異常情況並在編譯時強制完善?(對程序產生「錯誤/異常」有無成熟的理論分析,得出一些有意義的結論?)
1、異常結果本來就是一種輸出結果,只是語法上進行了特殊處理而已,對於支持模式匹配語法的函數式,很多就直接用模式匹配直接處理異常返回了。
2、編譯階段是可以枚舉出所有的異常並在編譯時強制完善的,Java就是這樣做的,需要顯示聲明throws。結果是這項特性貌似除了噁心人沒有什麼特別的優勢。
異常處理其實可以認為就是這樣做的:var result = invokeTryBlock();
if ( result is Exception )
{
//catch 模式匹配
return resu/若沒有處理異常,則拋到上級處理。 } else return resu/返回正確結果。
都異常了,還能怎麼處理?難道還能在REPL中恢復到call/cc的位置?
先申明沒有Go的實際經驗,只是對語言本身有興趣做過一些調查。
應該說Java和Go各有各的好處。
Java不區分錯誤和異常,由於不支持多返回值,錯誤/異常的處理方式基本都是遇到問題拋出異常,在調用端try/catch。但是由於Java強制規定必須明確聲明異常,所以優點在於調用一個方法你知道該處理那些異常,如果你處理不了,你就繼續往外扔。從語言層面確保了每個錯誤/異常都應該被處理。
(以下關於Go的認識可能會有很大偏差,請輕拍)
Go區分錯誤和異常是個優雅的設計,也符合實際業務場景。錯誤一般通過返回error,而異常通過panic/recover處理。光光這個層面Java和Go沒多大區別,但是語言層面error也只是個返回值,一方面存在nil的可能性,所以就被吐槽一半代碼都在判斷err==nil,另一方面沒辦法在編譯期確保所有錯誤都被處理。(panic/recover不太了解就不亂說話了)
就暫時的理解而言,我更喜歡java的錯誤/異常處理機制。
erlang/otp supervisor...
因為最近在看Golang的書,被洗腦後覺得Golang的錯誤處理機制很優雅,把錯誤作為一個值,或者直接panic朝上拋。因為取消了異常,分析代碼時條理很清楚,會減少很多負擔。舉個栗子可以用一個閉包函數捕獲所有異常和panic
func main(){
defer func(){
err := recover()
if err != nil{
fmt.Println(err)
}()
func1()
func2()
....
}
或者直接把錯誤用_忽視掉。
(不過還是覺得java的try-catch顯式處理每個可能異常更嚴謹。。。)知乎上有關於Go語言錯誤處理的討論Go 語言的錯誤處理機制是一個優秀的設計嗎? - Go 語言,Go的官博也有一篇文章Errors are values 好像要翻牆。推薦閱讀:
※為什麼C++中,析構函數、operator delete、以及operator delete []按照慣例不會拋出異常?
※c++ 程序運行時異常處理,怎麼定位到出錯代碼行?
※如何優雅的處理(或忽略)c++函數返回值代表的錯誤?
※python程序報錯後除了try except之外有沒有好的辦法再次啟動?
TAG:異常處理 |