我一直以來對於exception的態度都是很明確的。首先exception是好的,否則就不會有絕大多數的語言都支援他了。其次,error code也沒什麼問題,只是需要一個前提——你的語言得跟Haskell一樣有monad和comonad。你看Haskell就沒有exception,大家也寫的很開心。為什麼呢?因為只要把返回帶error code結果的函數給做成一個monad/comonad,那麼就可以用CPS變換把它變成exception了。所以說CPS作為跟goto同樣基本的控制流程語句真是當之無愧呀,只是CPS是type rich的,goto是type poor的。
其實很多人對於exception的恐懼心理在於你不知道一個函數會拋什麼exception出來,然後程式一crash你就傻逼了。對於server來講情況還好,出了問題只要殺掉快速重啟就行了,如今沒個replication和fault tolerance還有臉說你在寫後端(所以不知道那些做web的人究竟在反對什麼)?這主要的問題還是在於client。只要client上面的東西還沒儲存,那你一crash資料就完蛋了是不是——當然這隻是你的想象啦,其實根本不是這樣子的。
我們的程式拋了一個access violation出來,和拋了其它exception出來,究竟有什麼區別呢?access violation是一個很奇妙的東西,一旦拋了出來就告訴你你的程式沒救了,繼續執行下去說不定還會有破壞作用。特別是對於C/C++/Delphi這類語言來說,你不小心把錯誤的東西寫進了什麼亂七八糟的指標裡面去,那會兒什麼事情都沒發生,結果程式跑著跑著就錯了。因為你那個算錯了得到的野指標,說不定是隔壁的不知道什麼object的成員變數,說不定是heap裡面的資料結構,或者說別的什麼東西,就這麼給你寫了。如果你寫了別的object的成員變數那封裝肯定就不管用了,這個類的不變數就給你破壞了。既然你的成員函數都是基於不變數來寫的,那這個時候出錯時必須的。如果你寫到了heap的資料結構那就更加呵呵呵了,說不定下次一new就崩了,而且你還不知道為什麼。
出了access violation以外的exception基本是沒什麼危害的,最嚴重的大概也就是網線被拔了,另一塊不是裝OS的硬碟突然壞了什麼的這種反正你也沒辦法但是好歹還可以處理的事情。如果這些exception是你自己拋出來的那就更可靠了——那都是計劃內的。只要程式未來不會進入access violation的狀態,那證明你現在所能拿到的所有變數,還有指標指向的memory,基本上都還是靠譜的。出了你救不了的錯誤,至少你還可以吧資料安全的儲存下來,然後讓自己重啟——就跟word一樣。但是你有可能會說,拿出了access violation怎麼就不能儲存資料了呢?因為這個時候記憶體都毀了,指不定你儲存資料的代碼new點東西然後掛了,這基本上是沒準的。
所以無論你喜歡exception還是喜歡error code,你所希望達到的效果本質上就是避免程式未來會進入access violation的狀態。想做到這一點,方法也是很簡單粗暴的——只要你在函數裡面把運行前該對函數做的檢查都查一遍就好了。這個無論你用exception還是用error code,寫起來都是一樣的。區別在於調用你的函數的那個人會怎麼樣。那麼我來舉個例子,譬如說你覺得STL的map實在是太傻比了,於是你自己寫了一個,然後有了一個這樣子的函數:
// exception版本Symbol* SymbolMap::Lookup(const wstring& name); // error code版本int SymbolMap::Lookup(const wstring& name, Symbol*& result); // 其實COM就是你們最喜歡的error code風格了,寫起來應該很開心才對呀,你們的雙重標準真嚴重HRESULT ISymbolMap::Lookup(BSTR name, ISymbol** result);
於是拿到了Lookup函數之後,我們就要開始來完成一個任務了,譬如說拿兩個key得到兩個symbol然後組合出一個新的symbol。函數的錯誤處理邏輯是這樣的,如果key失敗了,因為業務的原因,我們要告訴函數外面說key不存在的。調用了一個ComposeSymbol的函數丟出什麼IndexOutOfRangeException顯然是不合理的。但是合并的那一步,因為業務都在同一個領域內,所以suppose裡面的異常外面是可以接受的。如果出現了計劃外的異常,那我們是處理不了的,只能丟給上面了,外面的代碼對於不認識的異常只需要報告工作失敗了就可以了。於是我們的函數就會這麼寫:
Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map){ Symbol* sa=0; Symbol* sb=0; try { sa=map->Lookup(a); sa=map->Lookup(b); } catch(const IndexOutOfRangeException& ex) { throw SymbolKeyException(ex.GetIndex()); } return CreatePairSymbol(sa, sb);}
看起來還挺不錯。現在我們可以開始考慮error code的版本了。於是我們需要思考幾個問題。首先第一個就是Lookup失敗的時候要怎麼報告?直接報告key的內容是不可能的,因為error code是個int。
題外話,error code當然可以是別的什麼東西,如果需要返回豐富內容的錯誤的話,那怎樣都得是一個指標了,這個時候你們就會面臨下面的問題——這已經他媽不滿足誰構造誰釋放的原則了呀,而且我這個指標究竟直接返回出去外面理不理呢,如果只要有一個環節不理了,那記憶體豈不是泄露了?如果我要求把錯誤返回在參數裡面的話,我每次調用函數都要建立出那麼個結構來儲存異常,不僅有if的複雜度,還有建立空間的複雜度,整個代碼都變成了屎。所以還是老老實實用int吧……