異常處理

在計算和計算機程式設計領域中，例外處理（exception handling，也意譯為異常處理，需注意 “” 一般對應英文 abnormality），是對出現的例外的響應處理，在程式期間，異常或例外情況需要特殊處理。一般而言，例外打斷正常的執行流程並執行預先登記的 “例外處理器”；具體如何去做依賴於它是硬體還是軟體例外，還有軟體例外是如何實現的。

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

硬體的例外處理

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

作業系統的例外處理

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

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

程式語言的例外處理

}}

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

程式語言對例外有著截然不同的定義，而現代語言大致上可分兩類：

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

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

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

3.1 歷史

在 1960 和 1970 年代，Lisp 語言發展出軟體例外。最初版本是在 1962 年 Lisp 1.5 的時候，這時候異常透過 ERRSET 關鍵詞進行捕捉，並在出錯時候，透過 NIL 進行回傳，而不是以前的終止程式或者進行偵錯程式。1960 年代後半，Maclisp 語言透過 ERR 關鍵詞引入 “引發”（Raise）錯誤機制。Lisp 的這種創新不僅僅被應用於丟擲錯誤，還被應用於 “非區域性控制流”。在在 1972 年 6 月，Maclisp 語言透過 CATCH 和 THROW 兩個新的關鍵詞來實現非區域性控制流，並保留 ERRSET 和 ERR 專門做錯誤處理。在 1970 中後，（“新實現的 LISP”）衍生出清除操作 UNWIND-PROTECT，對應著現今常見的 finally。該操作也被 Common Lisp 使用了。與之同時代，Scheme 也誕生了 dynamic-wind，用於處理閉包中的異常。和是介紹結構化的異常處理的開創性文章。 1980 年後，異常處理被廣泛利用於許多程式語言。

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

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

3.2 批評

1980 年 Tony Hoare 在評論 Ada 語言時，將異常處理提及為危險特徵。

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

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

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

3.3 程式語言相關支援

}}

許多常見的程式設計語言支援異常處理，包括：

Actionscript
Ada
BlitzMax
C++
C#
Common Lisp
D
ECMAScript
Eiffel
Java
ML
Modula-2
Modula-3
Object Pascal
Objective-C
OCaml
PHP（版本 5）
PL/I
Prolog
Python
REALbasic
Ruby
Visual Prolog
Scheme
大多數.NET 語言

多數語言的異常機制的語法是類似的：用 throw 或 raise 丟擲一個異常物件（Java 或 C++ 等）或一個特殊可擴充套件的列舉型別的值（如 Ada 語言）；異常處理程式碼的作用範圍用標記子句（try 或 begin 開始的語言作用域）標示其起始，以第一個異常處理子句（catch, except, rescue 等）標示其結束；可連續出現若干個異常處理子句，每個處理特定型別的異常。某些語言允許 else 子句，用於無例外出現的情況。更多見的是 finally, ensure 子句，無論是否出現異常它都將執行，用於釋放異常處理所需的一些資源。

C 語言沒有 try-catch 異常處理，而是使用用於錯誤檢查；setjmp 與 longjmp 標準庫函式可以被用來透過宏實現 try-catch 處理。一般在異常處理程式碼的搜尋過程中會逐級完成棧捲回（stack unwinding）；但 Common Lisp 中進行異常處理的條件系統，不採取棧捲回，因此允許異常處理完後在丟擲異常的程式碼處原地恢復執行。

3.3.1 C++

C++ 異常處理是資源獲取即初始化（RAII）的基礎。異常事件在 C++ 中表示為 “異常物件”（exception object）。異常事件發生時，由作業系統為程式設定當前異常物件，然後執行程式的當前異常處理程式碼塊，在包含了異常出現點的最內層的 try 塊，依次匹配同級的 catch 語句。如果匹配 catch 語句成功，則在該 catch 塊內處理異常；然後執行當前 try...catch... 塊之後的程式碼。如果在當前的 try...catch... 塊沒有能匹配該異常物件的 catch 語句，則由更外一層的 try...catch... 塊處理該異常；如果當前函式內的所有 try...catch... 塊都不能匹配該異常，則遞歸回退到呼叫棧的上一層函式去處理該異常。如果一直回退到主函式 main 都不能處理該異常，則呼叫系統函式 terminate 終止程式。

3.3.2 Python

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

Python 語言中對例外處理機制的採用是非常普遍深入的，這種編碼風格被稱為 EAFP（請求原諒比得到許可更容易），它假定有效的鍵或特性存在，並在這個假定證明失敗時捕獲例外。Python 社群認為這種風格是清晰而快速的，它的特徵是會出現很多 try 和 except 語句。這種技術對立於常見於很多其他語言比如 C 語言中的 LBYL（看好再跳）風格。

3.3.3 Java

在 Java 中異常是異常事件（exceptional event）的縮寫。異常是一個事件，它發生在程式執行時並會打亂程式指示的正常流程。當方法出現了錯誤時，方法會建立一個物件並將它交給執行時系統，所建立的物件叫 “異常物件”，該物件包含了錯誤的資訊（描述了出錯時的程式的型別和狀態）。建立錯誤物件和轉交給執行時系統的過程，叫丟擲異常。

class RuntimeException 和 class Error 均是不檢查的異常（Unchecked Exceptions）。錯誤不等於錯誤類（class Error），錯誤類代表著不應該被捕捉的嚴重的問題。class RuntimeException 意味著程式出現問題了。

3.3.4 Go

Go 語言提倡的是錯誤處理（error handling）。Go 語言設計者系統希望使用者在錯誤出時，顯式地檢查錯誤。 Go 雖然不提供與 Java 語言的 try..catch 同等的功能語句，但是取而代之，提供了輕型的異常處理機制 panic...recover。

3.3.5 .NET 語言

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

3.4 錯誤處理

錯誤處理（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 值判斷是否執行失敗，但有時候也會透過異常處理判斷是否執行失敗。

3.5 技術問題

3.5.1 未捕捉異常

如果一個異常丟擲後，沒有被捕捉，那麼未捕捉異常（uncaught exception）將會在執行時被處理。進行該處理的例程叫 “未捕捉異常處理器”。大部分的處理是終止程式並將錯誤資訊列印至控制檯，該資訊通常包含除錯用的資訊，如：異常的描述資訊、棧追蹤。通常處於最高階（應用級別）的處理器，即便捕捉到異常也會避免終止自身（如：執行緒出現異常，主執行緒也不會終止）。

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

作為預設的未捕捉異常處理器是可以被替換的，不管是全域性還是單執行緒的，新的未捕捉異常處理器可以嘗試做這些事情：未捕捉異常導致關閉了的執行緒，使之重啟；提供另一種方式記錄日誌；讓使用者報告未捕捉異常等等。在 Java 中，單一執行緒可以使用 Thread.setUncaughtExceptionHandler，全域性可以用 Thread.setDefaultUncaughtExceptionHandler；在 python 中，可透過修改 sys.excepthook。

3.5.2 異常的靜態檢查

3.5.2.1 檢查性異常

Java 的設計者設計了 檢查性異常（Checked exceptions）。當方法引發 “檢查性異常” 時，“檢查性異常” 將成為方法符號的一部分。例如：如果方法丟擲了 IOException ，我們必須顯式地使用方法符號（在 Java 中是 try...catch），如果不這樣做的話將會導致編譯時錯誤。

3.5.3 異常安全

一段程式碼是 “異常安全的”，如果這段程式碼執行時的失敗不會產生有害後果，如記憶體洩露、儲存資料混淆、或無效的輸出。異常安全可分成不同層次：

“失敗透明”，也稱作 “不丟擲保證”：程式碼的執行保證能成功並滿足所有的約束條件，即使存在異常情況。如果出現了異常，將不會對外進一步丟擲該異常。（異常安全的最好的層次）
“提交或捲回的語義”，或稱作 “強異常安全” 或 “無變化保證”：執行可以是失敗，但失敗的執行保證不會有負效應，因此所有涉及的資料都保持程式碼執行前的初始值。
“基本異常安全”：失敗執行的已執行的操作可能引起了副作用，但會保證狀態不變。所有儲存資料保持有效值，即使這些資料與異常發生前的值有所不同。
“最小異常安全”，也稱作 “無洩漏保證”：失敗執行的已執行的操作可能在儲存資料中儲存了無效的值，但不會引起崩潰，資源不會洩漏。
“沒有異常安全”：沒有保證（最差的異常安全層次）。

例如，考慮一個 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 並未實際插入，使得內部狀態不一致。對於 “異常不安全” 的實現，程式可能會崩潰，例如寫入資料到無效的記憶體。

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