異常處理

局势计算机编程

計算計算機編程領域中,例外處理(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). 

參考文獻

編輯

外部連結

編輯