例外處理

**例外處理**（Exception handling，中國大陸所用「异常」對應的英文是 Abnormality，港澳台以及日本使用的是「例外」）是指在進行運算（ computation）時，出現例外的情況（需要特殊處理的非常規或例外的情況）對應的處理，這種情況經常會破壞程式正常的流程。它通常由特殊的程式語言結構、電腦硬體機制（如：中斷或者如訊號等作業系統 IPC 設施）所構成的。具體實現由硬體和軟體自身定義而決定。一些例外，尤其是硬體，將會在被中斷後進行恢復。

## 硬體領域

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

## 作業系統提供的例外處理設施

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

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

## 軟體領域

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

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

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

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

Kiniry 強調「語言的設計僅僅部分地影響了例外機制的使用，結果上，（在整個系統的執行期間）形成的對異常使用的態度會處理影響部分或者所有的失敗（錯誤）。另外，其他主要的影響還有範例、核心代碼的編寫、技術書籍雜誌文章以及相關討論」。

### 歷史

在 1960 和 1970 年代，Lisp 語言發展出軟體例外。最初版本是在 1962 年 Lisp 1.5 的時候，這時候異常通過 `ERRSET` 關鍵詞進行捕捉，並在出錯時候，通過 `NIL` 進行回傳，而不是以前的終止程式或者進行除錯器。1960 年代後半，MacLisp 語言通過 `ERR` 關鍵詞引入**引發**（Raise）**錯誤**機制。Lisp 的這種創新不僅僅被應用於丟擲錯誤，還被應用於**非本地控制流（non-local control flow）** 。在在 1972 年 6 月，MacLisp 語言通過 `CATCH` 和 `THROW` 兩個新的關鍵詞來實現非本地控制流，並保留 `ERRSET` 和 `ERR` 專門做錯誤處理。在 1970 中後，`NIL` 衍生清除（Cleanup）操作（LISP 的新功能），對應著現今常見的 `finally`。該操作也被 Common Lisp 使用了。與之同時代，Scheme 也誕生了 `dynamic-wind`，用於處理 closures 中的異常。Goodenough (1975a) and Goodenough (1975b) 是首篇文章介紹結構化的例外處理。 1980 年後，例外處理被廣泛利用於許多程式語言。

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

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

### 中止語句

### 爭論

1980 年 Tony Hoare 在例外處理上提出了反對意見，這樣描述 Ada 語言時，認為例外處理是十分危險的。

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

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

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

### 錯誤處理

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

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

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

### 未捕捉異常 (Uncaught exceptions)

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

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

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

### 異常的靜態檢查 (Static checking of exceptions)

#### 檢查性異常（Checked exceptions）

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

### 程式語言相關支援

許多常見的程式設計語言，包括 Actionscript，Ada，BlitzMax，C++，C#，D，ECMAScript，Eiffel，Java，ML，Object Pascal（如 Delphi，Free Pascal 等），Objective-C，OCaml，PHP（version 5），PL/I，Prolog，Python，REALbasic，Ruby，Visual Prolog 以及大多數.NET 程式設計語言，內建的異常機制都是沿著函式呼叫堆疊的函式呼叫逆向搜尋，直到遇到例外處理代碼為止。一般在這個例外處理代碼的搜尋過程中逐級完成堆疊輾轉開解（stack unwinding）。但 Common Lisp 是個例外，它不採取堆疊輾轉開解，因此允許例外處理完後在丟擲異常的代碼處原地恢復執行。而 Visual Basic（尤其是在其早於 .net 的版本，例如 6.0 中）走得更遠：`on error` 語句可輕易指定發生異常後是重試（`resume`）還是跳過（`resume next`）還是執行程式設計師定義的錯誤處理程式（`goto ***`）。

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

C++ 例外處理是資源取得即初始化（Resource-Acquisition-Is-Initialization）的基礎。

C 語言一般認為是不支援例外處理的。Perl 語言可選擇支援結構化例外處理（structured exception handling）。

Python 語言對例外處理機制是非常普遍深入的，所以想寫出不含 `try, except` 的程式非常困難。

#### Python

在 python 里只存在異常與語法錯誤（syntax errors）。語法錯誤是在執行之前發生的。而異常是在執行時發生的錯誤，它將無條件停止程式，除非進行捕捉處理。

#### Java

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

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

#### Go

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

## 異常安全

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

1. **失敗透明（failure transparency）** ，也稱作**不丟擲保證（no throw guarantee）** ：代碼的執行保證能成功並滿足所有的約束條件，即使存在異常情況。如果出現了異常，將不會對外進一步丟擲該異常。（異常安全的最好的層次）
2. **提交或卷回的語意（commit or rollback semantics）** ，或稱作**強異常安全（strong exception safety）** 或**無變化保證（no-change guarantee）** ：執行可以是失敗，但失敗的執行保證不會有負效應，因此所有涉及的資料都保持代碼執行前的初始值。
3. **基本異常安全（basic exception safety）** ：失敗執行的已執行的操作可能引起了副作用，但會保證狀態不變。所有儲存資料保持有效值，即使這些資料與異常發生前的值有所不同。
4. **最小異常安全（minimal exception safety）** 也稱作**無洩漏保證（no-leak guarantee）** ：失敗執行的已執行的操作可能在儲存資料中儲存了無效的值，但不會引起崩潰，資源不會洩漏。
5. **異常不安全（no exception safety）** ：沒有保證（最差的異常安全層次）。

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

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

## 參考文獻