例外處理

局势计算机编程

計算電腦編程領域中,例外處理(exception handling,也意譯為例外處理,需注意「异常」一般對應英文abnormality[1]),是對出現的例外的響應處理,在程式執行英語Execution (computing)期間,異常或例外情況需要特殊處理。一般而言,例外打斷正常的執行流程並執行預先登記的「例外處理器」;具體如何去做依賴於它是硬件還是軟件例外,還有軟件例外是如何實現的。

例外是由電腦系統的不同層級來定義的,典型的層級有CPU定義的中斷作業系統(OS)定義的訊號程式語言定義的例外。每個層級都要求不同例外處理方式,但是它們可以是關聯的,比如說CPU中斷可能被轉變成OS訊號。一些例外特別是硬件例外,可以被優雅地處理使得程式執行能在它被中斷的地方恢復。

硬件的例外處理

編輯

硬件的例外處理機制由CPU完成。這種機制支援錯誤檢測,在發生錯誤後會將程式流跳轉到專門的錯誤處理常式中。發生異常前的狀態儲存在棧上。[2]

作業系統的例外處理

編輯

針對程式中可能發生的例外,作業系統可能通過IPC來提供對應的處理設施。行程執行過程中發生的中斷通常由操作提供的「中斷服務子程式」處理,作業系統可以藉此向該行程傳送訊號。行程可以通過註冊訊號處理器的方式自行處理訊號,也可以讓作業系統執行預設行為(比如終止該程式)。

從行程的視角,硬件中斷相當於可恢復異常,雖然中斷一般與程式流本身無關。

程式語言的例外處理

編輯

程式語言領域,通常例外(英語:exception)這一術語所描述的是一種資料結構,該資料結構可以儲存例外的相關訊息。例外處理的常見的一種機制是移交控制權。引發(raise)異常,也叫作投擲(throw)異常,通過該方式達到移交控制權的效果。例外投擲後,控制權會被移交至某處的(catch),並執行處理。

程式語言對例外有着截然不同的定義,而現代語言大致上可分兩類:[3]

  • 用作於控制流程的例外,如:Ada、Java、Modula-3、ML、OCaml、Python、Ruby 。
  • 用作於處理異常、無法預測、錯誤性的情況。如:C++[4]、C#、Common Lisp、Eiffel、Modula-2 。

子程式作者的角度看,如果要表示當前子程式無法正常執行,投擲例外是很好的選擇。無法正常執行的原因可以是輸入參數無效(比如值在函數的定義域之外),也可以是無法獲得所需的資源(比如檔案不存在、硬碟出錯、主記憶體不足)等等。在不支援例外的系統中,子程式需要通過返回特殊的錯誤碼英語Error code實作類似的功能。然而回傳錯誤碼可能導致不完全預測問題英語Semipredicate problem,子程式的使用方需要編寫額外的代碼,才能將普通的回傳值與錯誤碼相區別。

Kiniry強調:「語言設計僅僅部分地影響了對例外的使用,從而影響編程者處理系統執行期間部分或所有失敗的方式。其他主要的影響還有在核心庫、技術書籍、雜誌文章、線上研討討論區和特定組織的代碼標準中的使用範例」。[5]

歷史

編輯

在1960和1970年代,Lisp語言發展出軟件例外。最初版本是在1962年Lisp 1.5的時候,這時候異常通過ERRSET關鍵詞進行捕捉,並在出錯時候,通過NIL進行回傳,而不是以前的終止程式或者進行除錯器。[6]1960年代後半,Maclisp語言通過ERR關鍵詞引入「引發」(Raise)錯誤機制。[6]Lisp的這種創新不僅僅被應用於投擲錯誤,還被應用於「非局部控制流」。在在1972年6月,Maclisp語言通過CATCHTHROW兩個新的關鍵詞來實現非局部控制流,並保留ERRSETERR專門做錯誤處理。在1970中後,NIL英語NIL (programming language)(「新實現的LISP」)衍生出清除操作UNWIND-PROTECT,對應着現今常見的finally[7]該操作也被Common Lisp使用了。與之同時代,Scheme也誕生了dynamic-wind,用於處理閉包中的異常。Goodenough (1975a)Goodenough (1975b)是介紹結構化的例外處理的開創性文章。[8] 1980年後,例外處理被廣泛利用於許多程式語言。

PL/I語言使用的是動態作用域例外,然而稍微現代的程式語言多用詞法作用域的例外。PL/I語言的例外處理包含事件(不是錯誤)、注意(Attention)、EOF、列舉了的變數的修改(Modification of listed variables)。雖然現在的一些程式語言支援不含錯誤資訊的例外,但是他們並不常見。

一開始,軟件的例外處理是包含可恢復的例外,它具有恢復語意,就像大部分的硬件例外一樣,以及不恢復的例外,它具有終止語意。但是,在1960和1970時代,在實踐中得出恢復語意是十分低效的(C++標準相關的討論可見[9]),因此恢復語意就很少再出現了,通常只能在類似Common Lisp和Dylan這種語言中見到。

批評

編輯

1980年Tony Hoare在評論Ada語言時,將例外處理提及為危險特徵。[10]

對於軟件而言,例外處理經常無法正確的處理,尤其是當這裏有多種來自不同原始碼的異常時。在對五百萬行Java代碼進行數據流分析時,我們發現了超過1300個例外處理。[11]這是1999-2004年的前沿報告以及他們的結論,Weimer和Necula寫到,異常是一個十分嚴峻的問題,他們會創造隱藏的控制流途徑,這種途徑是編程人員很難去推理的。

Go語言的初始版本並沒有例外處理,而因此被有的開發者認為控制流十分冗餘。[12]後來,追加了類似的例外處理的語法panic/recover機制,但是Go語言的作者建立這僅僅在整個程式不可恢復的錯誤時候使用它。[13][14][15][16]

異常,作為一個非結構化的流程,它會增加資源泄露的可能性(如:從鎖住的代碼中逃脫,在打開檔案時候逃脫掉),也有可能導致狀態不一致。因此,出現了集中例外處理的資源管理技術,最常見的結合dispose pattern和解除保護(unwind protection)一起使用(如finally陳述式),會在這段代碼的控制權結束時自動釋放資源。

程式語言相關支援

編輯

許多常見的程式語言支援例外處理,包括:

多數語言的異常機制的語法是類似的:用throwraise投擲一個異常對象(Java或C++等)或一個特殊可延伸的列舉類型的值(如Ada語言);例外處理代碼的作用範圍用標記子句(trybegin開始的語言作用域)標示其起始,以第一個例外處理子句(catch, except, rescue等)標示其結束;可連續出現若干個例外處理子句,每個處理特定類型的異常。某些語言允許else子句,用於無例外出現的情況。更多見的是finally, ensure子句,無論是否出現異常它都將執行,用於釋放例外處理所需的一些資源。

C語言沒有try-catch例外處理,而是使用返回碼英語Error code用於錯誤檢查;setjmplongjmp標準庫函數可以被用來通過宏實現try-catch處理[17]。一般在例外處理代碼的搜尋過程中會逐級完成堆疊輾轉開解(stack unwinding);但Common Lisp中進行例外處理的條件系統,不採取堆疊輾轉開解,因此允許例外處理完後在投擲異常的代碼處原地恢復執行。

C++例外處理資源取得即初始化(RAII)的基礎。異常事件在C++中表示為「異常對象」(exception object)。異常事件發生時,由作業系統為程式設置當前異常對象,然後執行程式的當前例外處理代碼塊,在包含了異常出現點的最內層的try塊,依次匹配同級的catch陳述式。如果匹配catch陳述式成功,則在該catch塊內處理異常;然後執行當前try...catch...塊之後的代碼。如果在當前的try...catch...塊沒有能匹配該異常對象的catch陳述式,則由更外一層的try...catch...塊處理該異常;如果當前函數內的所有try...catch...塊都不能匹配該異常,則遞歸回退到呼叫棧的上一層函數去處理該異常。如果一直回退到主函數main()都不能處理該異常,則呼叫系統函數terminate()終止程式。

Python

編輯

Python中只存在語法錯誤和例外。語法錯誤是在執行之前發生的。而例外是在執行時發生的錯誤,除非進行捕捉處理,否則它將無條件停止程式。可以書寫代碼來處理選定的例外。[18]

Python語言中對例外處理機制的採用是非常普遍深入的,這種編碼風格被稱為EAFP(請求原諒比得到許可更容易)[19],它假定有效的鍵或特性存在,並在這個假定證明失敗時擷取例外。Python社區認為這種風格是清晰而快速的,它的特徵是會出現很多tryexcept陳述式。這種技術對立於常見於很多其他語言比如C語言中的LBYL(看好再跳)風格。

Java中異常是異常事件(exceptional event)的縮寫。異常是一個事件,它發生在程式執行時並會打亂程式指示的正常流程。當方法出現了錯誤時,方法會建立一個對象並將它交給執行時系統,所建立的對象叫「異常對象」,該對象包含了錯誤的資訊(描述了出錯時的程式的類型和狀態)。建立錯誤對象和轉交給執行時系統的過程,叫投擲異常。[20]

class RuntimeExceptionclass Error均是不檢查的異常(Unchecked Exceptions)。[21]錯誤不等於錯誤類(class Error),錯誤類代表着不應該被捕捉的嚴重的問題。[22]class RuntimeException 意味着程式出現問題了。[21]

Go語言提倡的是錯誤處理(error handling)。Go語言設計者系統希望用戶在錯誤出時,顯式地檢查錯誤。[23] Go雖然不提供與Java語言的try..catch同等的功能陳述式,但是取而代之,提供了輕型的例外處理機制panic...recover[24]

.NET語言

編輯

大多數.NET程式語言,內建的異常機制都是沿着函數呼叫堆疊的函數呼叫逆向搜尋,直到遇到例外處理代碼為止。而 Visual Basic(尤其是在其早於 .net 的版本,例如 6.0 中)走得更遠:on error 陳述式可輕易指定發生異常後是重試(resume)還是跳過(resume next)還是執行程式設計師定義的錯誤處理程式(goto ***)。

錯誤處理

編輯

錯誤處理(error handling)是通過處理常式的返回值的形式從而處理錯誤的一種編程方式。在Go等返回值可為複數的語言中,可通過將其中一個值設為錯誤值,從而達到錯誤處理的效果。

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

在僅僅支援返回狀態碼的語言裏,可通過處理錯誤碼,達到錯誤處理的效果。shell語言可通過$?獲得函數執行的退出碼,從而判斷是否出錯。

在其他語言中,可以通過判斷結果的某一個特徵,從而達到錯誤處理部分的效果,但不意味着這些語言自身支援錯誤處理。如,Java等物件導向的語言往往會通過null值判斷是否執行失敗,但有時候也會通過例外處理判斷是否執行失敗。

技術問題

編輯

未捕捉異常

編輯

如果一個異常投擲後,沒有被捕捉,那麼未捕捉異常(uncaught exception)將會在執行時被處理。進行該處理的常式叫「未捕捉例外處理器」(uncaught exception handler[25][26]。大部分的處理是終止程式並將錯誤資訊列印至控制台,該資訊通常包含除錯用的資訊,如:異常的描述資訊、棧追蹤[27][28][29]通常處於最進階(應用級別)的處理器,即便捕捉到異常也會避免終止自身(如:線程出現異常,主線程也不會終止)。[30][31]

值得了解的是,在即便未捕捉異常導致了程式異常中斷(如:異常沒被捕捉、捲動未完成、沒釋放資源),程式仍舊能正常地順序性地關閉。只要確保執行時系統能正常地執行,因為執行時系統控制着整個程式的執行。

作為預設的未捕捉例外處理器是可以被替換的,不管是全域還是單線程的,新的未捕捉例外處理器可以嘗試做這些事情:未捕捉異常導致關閉了的線程,使之重新啟動;提供另一種方式記錄紀錄檔;讓用戶報告未捕捉異常等等。在Java中,單一線程可以使用Thread.setUncaughtExceptionHandler[32],全域可以用Thread.setDefaultUncaughtExceptionHandler[33];在python中,可通過修改sys.excepthook[34]

異常的靜態檢查

編輯
檢查性異常
編輯

Java的設計者設計了[35] 檢查性異常(Checked exceptions)[36]。當方法引發「檢查性異常」時,「檢查性異常」將成為方法符號的一部分。例如:如果方法投擲了IOException ,我們必須顯式地使用方法符號(在Java中是try...catch),如果不這樣做的話將會導致編譯時錯誤。

異常安全

編輯

一段代碼是「異常安全的」,如果這段代碼執行時的失敗不會產生有害後果,如主記憶體泄露、儲存數據混淆、或無效的輸出。異常安全可分成不同層次:

  1. 「失敗透明」,也稱作「不投擲保證」:代碼的執行保證能成功並滿足所有的約束條件,即使存在異常情況。如果出現了異常,將不會對外進一步投擲該異常。(異常安全的最好的層次)
  2. 「提交或卷回的語意」,或稱作「強異常安全」或「無變化保證」:執行可以是失敗,但失敗的執行保證不會有負效應,因此所有涉及的數據都保持代碼執行前的初始值。[37]
  3. 「基本異常安全」:失敗執行的已執行的操作可能引起了副作用,但會保證狀態不變。所有儲存數據保持有效值,即使這些數據與異常發生前的值有所不同。
  4. 「最小異常安全」,也稱作「無泄漏保證」:失敗執行的已執行的操作可能在儲存數據中儲存了無效的值,但不會引起崩潰,資源不會泄漏。
  5. 「沒有異常安全」:沒有保證(最差的異常安全層次)。

例如,考慮一個smart vector類型,如C++的 std::vector或Java的 ArrayList。當一個數據項x插入vector v,必須實際增加x的值到vector的內部對象列表中並且修改vector的計數域以正確表示v中儲存了多少數據項;此時如果已有的儲存空間不夠大,就需要分配新的主記憶體。主記憶體分配可能會失敗並投擲異常。因此,vector資料類型如果是「失敗透明」保證將會非常困難甚至不可能實現。但vector類型提供「強異常安全」保證卻是相當容易的;在這種情況下,x插入v或者成功,或者v保持不變。如果vector類型僅提供「基本異常安全」保證,如果數據插入失敗,v可能包含也可能不包含x的值,但至少v的內部表示是一致的。但如果vector資料類型是「最小異常安全」保證,v可能會是無效的,例如v的計數域被增加了,但x並未實際插入,使得內部狀態不一致。對於「異常不安全」的實現,程式可能會崩潰,例如寫入數據到無效的主記憶體。

通常至少需要基本異常安全。失敗透明是難於實現的,特別是在編寫庫函數時,因為對應用程式的複雜知識缺少獲知。

參照

編輯
  1. ^ abnormality汉语(繁体)翻译:剑桥词典. dictionary.cambridge.org. [2020-02-04]. (原始內容存檔於2021-04-14) (中文(簡體)). 
  2. ^ Hardware Exceptions Detection. TEXAS INSTRUMENTS. 2011-11-24 [2012-10-05]. (原始內容存檔於2013-11-10) (英語). 
  3. ^ Kiniry, J. R. Exceptions in Java and Eiffel: Two Extremes in Exception Design and Application. Advanced Topics in Exception Handling Techniques. Lecture Notes in Computer Science 4119. 2006: 288–300. ISBN 978-3-540-37443-5. doi:10.1007/11818502_16. 
  4. ^ Stroustrup: C++ Style and Technique FAQ. www.stroustrup.com. [5 May 2018]. (原始內容存檔於2 February 2018). 
  5. ^ Kiniry, J. R. Exceptions in Java and Eiffel: Two Extremes in Exception Design and Application. Advanced Topics in Exception Handling Techniques. Lecture Notes in Computer Science 4119. 2006: 288–300. ISBN 978-3-540-37443-5. doi:10.1007/11818502_16. 
  6. ^ 6.0 6.1 Gabriel & Steele 2008,第3頁.
  7. ^ White 1979,第194頁.
  8. ^ Stroustrup 1994,第392頁.
  9. ^ Stroustrup 1994,16.6 Exception Handling: Resumption vs. Termination, pp. 390–393.
  10. ^ C.A.R. Hoare. The Emperor's Old Clothes. 1980 [2024-01-13]. (原始內容存檔於2024-04-15). I have been giving the best of my advice to this project since 1975. At first I was extremely hopeful. The original objectives of the language, included reliability, readability of programs, formality of language definition, and even simplicity. Gradually these objectives have been sacrificed in favor of power, supposedly achieved by a plethora of features and notational conventions, many of them unnecessary and some of them, like exception handling, even dangerous. 
  11. ^ Weimer, W; Necula, G.C. Exceptional Situations and Program Reliability (PDF) 30 (2). 2008. (原始內容存檔 (PDF)於2015-09-23).  |journal=被忽略 (幫助)
  12. ^ Frequently Asked Questions. [2017-04-27]. (原始內容存檔於2017-05-03). We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional. 
  13. ^ Panic And Recover 互聯網檔案館存檔,存檔日期2013-10-24., Go wiki
  14. ^ Weekly Snapshot History. golang.org. (原始內容存檔於2017-04-03). 
  15. ^ Proposal for an exception-like mechanism. golang-nuts. 25 March 2010 [25 March 2010]. (原始內容存檔於2013-03-06). 
  16. ^ Effective Go. golang.org. (原始內容存檔於2015-01-06). 
  17. ^ Roberts, Eric S. Implementing Exceptions in C (PDF). DEC Systems Research Center. 21 March 1989 [4 January 2022]. SRC-RR-40. (原始內容存檔 (PDF)於2022-01-05). 
  18. ^ 8. Errors and Exceptions — Python 3.8.1 documentation. docs.python.org. [2020-02-04]. (原始內容存檔於2022-06-08). 
  19. ^ Python documentation — Glossary — EAFP. [2024-01-12]. (原始內容存檔於2020-06-25). 
  20. ^ What Is an Exception? (The Java™ Tutorials > Essential Classes > Exceptions). docs.oracle.com. [2020-02-04]. (原始內容存檔於2022-06-09). 
  21. ^ 21.0 21.1 Unchecked Exceptions — The Controversy (The Java™ Tutorials > Essential Classes > Exceptions). docs.oracle.com. [2020-02-04]. (原始內容存檔於2022-06-07). 
  22. ^ Error (Java Platform SE 8 ). docs.oracle.com. [2020-02-04]. (原始內容存檔於2021-10-24). 
  23. ^ Error handling and Go - The Go Blog. blog.golang.org. [2020-02-04]. (原始內容存檔於2021-07-12). 
  24. ^ Google 网上论坛. groups.google.com. [2020-02-04]. (原始內容存檔於2011-01-22). 
  25. ^ Mac Developer Library, "Uncaught Exceptions 互聯網檔案館存檔,存檔日期2016-03-04."
  26. ^ MSDN, AppDomain.UnhandledException Event 互聯網檔案館存檔,存檔日期2016-03-04.
  27. ^ Mac Developer Library, "Uncaught Exceptions 互聯網檔案館存檔,存檔日期2016-03-04."
  28. ^ The Python Tutorial, "8. Errors and Exceptions 互聯網檔案館存檔,存檔日期2015-09-01."
  29. ^ Java Practices -> Provide an uncaught exception handler. www.javapractices.com. [5 May 2018]. (原始內容存檔於9 September 2016). 
  30. ^ Mac Developer Library, "Uncaught Exceptions 互聯網檔案館存檔,存檔日期2016-03-04."
  31. ^ Exception Handling — PyMOTW 3. pymotw.com. [2020-02-03]. (原始內容存檔於2021-05-16). 
  32. ^ Thread.setUncaughtExceptionHandler. [2020-02-03]. (原始內容存檔於2024-07-31). 
  33. ^ Thread.setDefaultUncaughtExceptionHandler. [2020-02-03]. (原始內容存檔於2024-07-31). 
  34. ^ sys.excepthook. [2020-02-03]. (原始內容存檔於2021-10-25). 
  35. ^ Google Answers: The origin of checked exceptions. [2011-12-15]. (原始內容存檔於2011-08-06). 
  36. ^ Java Language Specification, chapter 11.2. http://java.sun.com/docs/books/jls/third_edition/html/exceptions.html#11.2 互聯網檔案館存檔,存檔日期2006-12-08.
  37. ^ 存档副本. [2011-08-13]. (原始內容存檔於2009-02-03). 

參考文獻

編輯

外部連結

編輯