如何考慮異常 教科書和類似的參考資料一直以來都集中在異常的語法和局部語義方面。通過它們的充分介紹,大多數 程序員 都能閱讀帶有異常的代碼並能解釋其作用。它們所欠缺的是一種對有效風格的感覺。要找到這種感覺,您需要, 重點了解您要求異常為您解決的
如何考慮異常 教科書和類似的參考資料一直以來都集中在異常的語法和局部語義方面。通過它們的充分介紹,大多數
程序員都能閱讀帶有異常的代碼並能解釋其作用。它們所欠缺的是一種對有效風格的感覺。要找到這種感覺,您需要,
重點了解您要求異常為您解決的問題,
如何捕獲異常,以及,
如何拋出異常。
本月的專欄文章列舉了幾個示例來說明如何實現上述三點。
在研究這幾個示例之前,不妨采用一種可能與您第一次學習異常時不同的方式來考慮異常,“熱熱身”。由您喜歡的語言提供的異常系統是不適合於最終用戶查看的。相反,可以把異常當作腳手架,在完成應用程序之後,再“拆除”這些“腳手架”。也許您曾在課堂上學過閱讀這樣的異常,如下所示
caught exception in main()
java.lang.SomeException: ugly input
at ...
當然,這個技巧對程序員是有價值的。可是它絕不能被強加給最終用戶。一個完整的應用程序應該從來不說“有異常”;所有呈現給最終用戶的報告都應該用下面的這種本機語言來寫,或許更接近於這樣
The configuration file 'folder/thing.cfg'
appears to be cor
rupt, as line #17 cannot
be parsed.
正是這種清晰性對應用程序的
安全性施加了直接的壓力。其原因在於:用戶及其管理員一次又一次地證明他們對不能理解的事物的反應,是簡化系統直至得到自己期望的行為。如果他們讀到“未發現文件(file not found)”,他們會隨意地從別處復制一些文件,而不考慮特權或許可權。而保證應用程序安全性的最可靠的方法之一就是使應用程序工作,這樣用戶才會理解它的運作。聰明的用戶會因為急於“讓程序工作”而破壞幾乎所有安全性設置。
不是所有程序員都認同我這種觀點。有不少高級
軟件工程師冷靜地提議說,用戶輸入錯誤的數據或錯用應用程序都是咎由自取。我在這裡不是要討論這種態度的道德問題;只是注意到,在
開發人員和最終用戶采取這種互相對立的姿態時,安全性正在不斷地被破壞。
因此,在某個特定開發項目中,使用異常的第一步,也往往是最容易被忽視的一步,就是確定程序對異常的需求。這一點一定要搞清楚。當客戶或主管在指示程序應如何處理格式良好的輸入數據時,抓住機會,對萬一發生錯誤時程序的具體操作細節同他們達成共識。給自己足夠的時間去會見客戶。想象一下:最終客戶可能會把同程序“接觸時間”的大部分都消耗在查看程序所顯示的錯誤消息上。這並不是駭人聽聞,對許多應用程序來說“正常”操作是相當快的,而對於錯誤的響應,人們需要花不少時間來思考。錯誤消息及對應操作同程序的其它部分相比,值得花同樣多的技術。
事實上,我會對那些讓最好的人才集中精力於錯誤處理方面,而不是傳統編程中比較“花哨”的方面(比如圖形用戶界面(GUI)外觀編程)的項目,更感到高興。理由在於:一個有錯誤但帶有優秀的錯誤處理機制的應用程序,比近乎完美但其錯誤處理機制卻不友好的應用程序,更能贏得最終用戶的歡心。
在完成了第一輪
需求分析後,您手中擁有的這些敘述性說明會使程序的異常處理設計更為合理且更有價值。現在的挑戰則是這篇專欄文章的讀者所感興趣的技術問題。
捕獲 Python 作為一種方便的工具,可用來表達示例用法。我經常遇到類似於這樣的
缺陷:
清單 1. 不匹配的捕獲
try:
process(some_file)
except:
alert("error in opening" '%s' % some_file)
發現問題沒有?異常的語法和語義不匹配,這有點類似於一個公務員,在向選民承諾要注意他們最關心的問題,尤其是游泳池開放時間。盡管這樣的語句形式上正確,其失衡卻會使聽眾感到震驚,暗示有更深層的問題。
上面這個不匹配的捕獲也存在類似問題:它捕獲了所有錯誤,但僅僅只報告了“打開文件時出現錯誤(error in opening)”。這樣寫會好一點:
清單 2. 均衡性較好的捕獲
try:
process(some_file)
except IOError:
alert("error in opening" '%s' % some_file)
許多程序員由衷地認為這兩個例子是等價的,因為一種可能的理由是,“記錄 process 只是用來生成 IOError”。在這層意義上講,應用程序在這兩個示例中的執行,的確毫無差別。可是源代碼不只是給計算機用的;更重要的是必須向身為人類的讀者表達其含意。如果您的代碼假設某個特定的異常一定是一個 IOError,那麼利用該語言的精確性,就這麼說。
第二個示例仍不能完全防止讓最終用戶看到“原始”異常的危險。實際上,即使 process() 在當前版本中被明白無誤地記錄為僅拋出 IOError,但我仍要求在編寫該段代碼時至少達到下面這樣的詳細程度:
清單 3. 形式均衡且全面的捕獲
try:
process(some_file)
except IOError:
alert("error in opening" '%s' % some_file)
except:
alert("internal and completely unexpected problem")
當然,對我們而言,擁有完整且正確記錄的接口是一種少有的奢侈。在開發工作中許多語言采用了一種有用的技術 - 使用異常系統內置的內省。這使我們的示例變成這樣:
清單 4. 形式均衡且全面的捕獲,並帶有信息性的“缺省設置”
try:
process(some_file)
except IOError:
alert("error in opening" '%s' % some_file)
except:
(exc_class, exc_object, exc_traceback) = sys.exc_info()
alert("""internal and completely unexpected problem,
manifested as %s""" % str(exc_class))
舉例來說,如果 process() 的實現產生了一個導致 ValueError 而不是 IOError 的錯誤,上面最後一個處理程序至少會將 ValueError 作為類名報告上來。
在捕獲時還常有另一種含糊不清之處。其代碼像這樣:
錯誤處理中過寬的作用域
try:
first_operation()
second_operation()
third_operation()
fourth_operation()
except:
alert("something went wrong")
這裡的不足之處在於“橫向”的不精確;當出現 something went wrong 時,沒有能立即與引發錯誤的特定 *_operation() 連接。一種簡單的
解決方案是一次只捕獲一段,這樣前面的編碼就變成:
清單 6. 錯誤處理中更高的精確度
# The documentation assures us these
# two can't toss exceptions.
first_operation()
second_operation()
try:
third_operation()
except:
alert("something went wrong in 'third_operation()'")
# This, also, cannot throw an exception.
fourth_operation()
稍微復雜一點的解決方案是讓捕獲代碼的范圍更寬一些,但要使用語言的內省能力來報告追溯信息:
清單 7. 許多語言能管理自己的追溯
try:
first_operation()
second_operation()
third_operation()
fourth_operation()
except:
exc_traceback = sys.exc_info()[2]
stack_list = []
while 1:
stack_list.append(exc_traceback.tb_frame.f_code.co_name)
if not exc_traceback.tb_next:
break
exc_traceback = exc_traceback.tb_next
# The next is an
almost-human-readable
# description of where the fault o
clearcase/" target="_blank" >ccurred
alert("something went wrong in %s" % stack_list)
於是,在捕獲異常時,確信獲取了全部異常且其處理方式是精確的,並使用您所選擇的語言中的可用信息合成出有用的輸出。
拋出
同異常的使用相比,生成異常是一個稍許高級的主題。但是,所有服務器診所的讀者都應知道拋出異常的基礎:
記錄接口。
保持簡單的繼承層次結構。
繼承是由語言支持的用於異常值的出色技術;按 IOError、ValueError、AppError 等類別組織錯誤是一種相當有用的方法。然而,沒有經驗的設計人員常將他們的繼承層次結構復雜化,以至於同最優的層次結構相差甚大。如果您發現在異常類中定義了兩層以上的繼承級別,那麼就要檢查一下。如果有三層以上,或者在一個異常超類中子類的個數超過了七個,那我打賭一定有什麼地方出錯了。
在這方面經常犯的一個錯誤是在 Exception 或者 Error 下復制了一棵應用程序對象樹。這幾乎總是是個錯誤,但是可以通過將異常參數化而不是子類化來簡單地修正這個錯誤。一個指定了
MercuryException 和 VenusException 等的設計可能不是最好的;而用 PlanetException 進行編碼,並附有數據指出哪一個 planet 是問題所在的設計可能會更好。
有一種稍許微妙的風格,可支持契約式設計(design-by-contact,DbC)方法,即在 AssertionError 之外放棄對任何異常類的定義,然後只用斷言來編碼所有異常接口。
高級異常 對於異常還有很多可學的,其中只有很小一部分由印刷出版的書籍詳細充分地介紹過;保護異常使它們不被用來破壞安全性;異常
度量;異常設計的
性能後果;調試、基准
測試及驗證異常的方法;異常和資源管理;以及更多其它內容。在本月,最後要提到的一點是,用多語言編碼的異常系統的重要性。
服務器診所經常提倡“雙級”編程 - 組合兩種不同語言 - 來利用各自的長處。如今這種方法已經十分流行和“普通”,有不少系統采用了這種方法,其中包括在兩種語言之間進行對象“轉換”。David Beazley 的簡化包裝器和接口生成器(Simplified Wrapper and Interface Generator,SWIG)就是用於這類工作的工具,並得到了極為廣泛地應用。
雖然之前有所忽視,但教會不同語言的異常系統相互之間如何進行有效的交流一直都是需要的。這正是芝加哥大學計算機科學系助理教授 Beazley 如今正面對的一項挑戰。他的包裝應用程序生成器(Wrapped Application Generator,WAD)“是一個用於簡化調試腳本編制語言擴展工作的
嵌入式調試系統”。