例外處理 (編程)

在電腦編程中，一些程式語言提供了用於例外處理（exception handling，或意譯為異常處理）的機制。例外（exception）這一術語所描述的通常是一種資料結構，該資料結構可以存儲例外的相關訊息。例外處理的常見的一種機制是移交控制權。引發（raise）例外，也叫作拋出（throw）例外，通過該方式達到移交控制權的效果。例外拋出後，控制權會被移交至某處的接住（catch），並執行處理。

## 概述

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

程式語言在例外是什麼的觀念上有著微妙的差異。例外可以被用來表示並處理異常（abnormal）、不可預測（unpredictable）、錯誤（erroneous）的情況，還可以用作流程控制結構來處理正常的情況。例如，Python迭代器拋出`StopIteration`例外來指示這個迭代器沒有項目可以產生了。在很多語言中，對例外的慣常用法該怎樣構成的存在分歧。例如，Joshua Bloch聲稱Java的例外處理應當只用於異常情況，但是Kiniry觀察到Java的java.io.FileNotFoundException類根本就不是異常事件。類似的，C++的創造者Bjarne Stroustrup聲稱C++例外應當只用於錯誤處理，這是它們的設計用途。但是Kiniry觀察到很多現代語言比如Ada、C++、Modula-3、ML、OCaml、Python和Ruby，將例外用於流程控制。一些語言比如Eiffel、C#、Common Lisp和Modula-2，做出了協同努力來限制其例外的使用，儘管這是在社會而非及技術層面進行的。

## 歷史

在1960和1970年代，Lisp語言發展出軟體例外。最初版本是在1962年Lisp 1.5的時候，這時候異常通過`ERRSET`關鍵詞進行捕捉，並在出錯時候，通過`NIL`進行回傳，而不是以前的終止程序或者進行調試器。1960年代後半，Maclisp語言通過`ERR`關鍵詞引入「引發」（Raise）錯誤機制。Lisp的這種創新不僅僅被應用於拋出錯誤，還被應用於「非局部控制流」。在在1972年6月，Maclisp語言通過`CATCH`和`THROW`兩個新的關鍵詞來實現非局部控制流，並保留`ERRSET`和`ERR`專門做錯誤處理。在1970中後，NIL (程式語言)（「新實現的LISP」）衍生出清除操作`UNWIND-PROTECT`，對應著現今常見的`finally`。該操作也被Common Lisp使用了。與之同時Scheme也提供了對續體調用進行「保護」的`dynamic-wind`，只要控制進入或離開它所界定範圍包括通過非局部跳轉方式都會分別執行特定的任務。和Goodenough是介紹結構化的異常處理的開創性文章。1980年後，異常處理被廣泛利用於許多程式語言。

PL/I語言在大約1964年介入自己形式的動態作用域例外，PL/I允許通過ON-unit來處理中斷。而稍微現代的程式語言大多採用詞法作用域的例外。

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

## 批評

對於軟體而言，異常處理經常無法正確的處理，尤其是當這裡有多種來自不同原始碼的異常時。研究者在對五百萬行Java代碼進行數據流分析時，發現了超過1300個異常處理。Weimer和Necula引述了前人的研究並給出了結論，異常是一個十分嚴峻的問題，它們會創造隱藏的控制流途徑，這種途徑是編程人員很難去推理的。

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

異常，作為一個非結構化的控制流程，會增加資源洩漏的可能性（比如從互斥鎖鎖住的臨界區段中逃脫，或從臨時保持打開一個文件的代碼段中逃脫），也有可能導致狀態不一致。因此，出現了集中異常處理的資源管理技術，最常見的結合處置模式和解開保護（unwind protection）一起使用（如`finally`語句），會在這段代碼的控制權結束時自動釋放資源。

Go開發者認為try-catch-finally慣用法會混淆控制流，並介入了類似異常的panic/recover機制。recover不同於catch之處在於它只能在一個函數中的defer代碼塊之內調用，所以處理器只能做清潔和變更這個函數的返回值，而不能將控制返回到這個函數內的任意點。defer塊自身功能類似於finally子句。

Rust語言沒有例外，它使用Result（一種結果類型）來處理運行時錯誤，而對於嚴重錯誤使用panic!宏。

## 程式語言相關支持

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

- 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中進行異常處理的條件系統，不採取堆疊輾轉開解，因此允許異常處理完後在拋出異常的代碼處原地恢復執行。

### C++

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

### Python

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

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

### Java

在Java中異常是異常事件（exceptional event）的縮寫。異常是一個事件，它發生在程序運行時並會打亂程序指示的正常流程。當方法出現了錯誤時，方法會創建一個對象並將它交給運行時系統，所創建的對象叫「異常對象」，該對象包含了錯誤的資訊（描述了出錯時的程序的類型和狀態）。創建錯誤對象和轉交給運行時系統的過程，叫拋出異常。

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

### .NET語言

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

## 異常處理的技術問題

### 未捕捉異常

如果一個異常拋出後，沒有被捕捉，那麼未捕捉異常（uncaught exception）將會在運行時被處理。進行該處理的例程叫「未捕捉異常處理器」（uncaught exception handler）。大部分的處理是終止程序並將錯誤信息列印至控制台，該資訊通常包含調試用的資訊，如：異常的描述資訊、棧追蹤。通常處於最高級（應用級別）的處理器，即便捕捉到異常也會避免終止自身（如：線程出現異常，主線程也不會終止）。

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

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

### 異常的靜態檢查

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

### 異常安全

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

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

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

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

## 外部連結

- Article " C++ Exception Handling " by Christophe de Dinechin
- Article " Exceptional practices " by Brian Goetz
- Article " Object Oriented Exception Handling in Perl " by Arun Udaya Shankar
- Article " Programming with Exceptions in C++ " by Kyle Loudon
- Article " Unchecked Exceptions - The Controversy "
- Descriptions from Portland Pattern Repository
- Does Java Need Checked Exceptions?