异常处理

例外處理（exception handling，中國大陆所用“”对应的英文是abnormality，港澳台以及日本使用的是“”）是指在进行运算（computation）时，出现的情况（需要特殊处理的非常规或的情况）对应的处理，这种情况经常会破坏程序正常的流程。它通常由特殊的编程语言结构、计算机硬體机制（如：中断或者如訊號等操作系统IPC设施）所构成的。具体实现由硬體和軟體自身定义而决定。一些例外，尤其是硬體，将会在被中断后进行恢復。

硬體领域 

硬體的异常处理机制由 CPU 完成。这种机制支持错误检测，在发生错误后会将程序流跳转到专门的错误处理过程（）中。发生异常前的状态存储在栈上。

操作系统提供的例外處理设施 

针对程序中可能发生的例外，操作系统可能通过 IPC 来提供对应的处理设施。进程执行过程中发生的中断通常由操作提供的「中断服务子程序」处理，操作系统可以藉此向该进程发送信号。进程可以通过注册信号处理器的方式自行处理信号，也可以让操作系统执行預設行为（比如终止该程序）。

从进程的视角，硬體中断相当于可恢复异常，虽然中断一般与程序流本身无关。

軟體领域 
在编程语言领域，通常 例外（）这一术语所描述的是一种資料结构，该資料结构可以存储异常（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中的异常。 and 是首篇文章介绍结构化的异常处理。 1980年后，异常处理被廣泛利用于许多编程语言。

PL/I语言使用的是动态域（Dynamically scoped）例外，然而稍微现代的编程语言多用词法作用域（lexically scoped</sup>的例外。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语言的初始版本并没有异常处理，而因此被有的开发者认为控制流十分冗余。后来，追加了类似的异常处理的语法/机制，但是Go语言的作者建立这仅仅在整个程序不可恢复的错误时候使用它。

异常，作为一个非结构化的流程，它会增加资源泄露的可能性（如：从锁住的代码中逃脱，在打开文件时候逃脱掉），也有可能导致状态不一致。因此，出现了集中异常处理的资源管理技术，最常见的结合dispose pattern和解除保护（unwind protection）一起使用（如finally语句），会在这段代码的控制权结束时自动释放资源。

错误处理 
错误处理（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） 
如果一个异常抛出后，没有被捕捉，那么未捕捉异常将会在运行时被处理。进行该处理的程序（routine）叫 未捕捉异常处理器（）。大部分的处理是终止程序并将错误信息打印至控制台，该信息通常包含调试用（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程序设计语言，内建的异常机制都是沿着函数-{zh-hans:调用栈;zh-hant:呼叫堆疊}-的函数调用逆向搜索，直到遇到异常处理代码为止。一般在这个异常处理代码的搜索过程中逐级完成-{zh-hans:栈卷回;zh-hant:堆疊輾轉開解;}-（stack unwinding）。但Common Lisp是个例外，它不采取-{zh-hans:栈卷回;zh-hant:堆疊輾轉開解;}-，因此允许异常处理完后在抛出异常的代码处原地恢复执行。而 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。

异常安全 
一段代码是异常安全的，如果这段代码运行时的失败不会产生有害后果，如内存泄露、存储数据混淆、或无效的输出。异常安全可分成不同层次：

 失败透明（failure transparency），也称作不抛出保证（no throw guarantee）：代码的运行保证能成功并满足所有的约束条件，即使存在异常情况。如果出现了异常，将不会对外进一步抛出该异常。（异常安全的最好的层次）
 提交或卷回的语义（commit or rollback semantics），或称作强异常安全（strong exception safety）或无变化保证（no-change guarantee）：运行可以是失败，但失败的运行保证不会有负效应，因此所有涉及的数据都保持代码运行前的初始值。
 基本异常安全（basic exception safety）：失败运行的已执行的操作可能引起了副作用，但会保证状态不变。所有存储数据保持有效值，即使这些数据与异常发生前的值有所不同。
 最小异常安全（minimal exception safety）也称作无泄漏保证（no-leak guarantee）：失败运行的已执行的操作可能在存储数据中保存了无效的值，但不会引起崩溃，资源不会泄漏。
 异常不安全（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并未实际插入，使得内部状态不一致。对于“异常不安全”的实现，程序可能会崩溃，例如写入数据到无效的内存。

通常至少需要基本异常安全。失败透明是难于实现的，特别是在编写库函数时，因为对应用程序的复杂知识缺少获知。

参考文献 

控制流程
軟體工程