异常处理
此條目可参照英語維基百科相應條目来扩充。 |
例外處理(Exception handling,中國大陆所用“异常”对应的英文是Abnormality[1],港澳台以及日本使用的是“例外”)是指在进行运算( computation)时,出现例外的情况(需要特殊处理的非常规或例外的情况)对应的处理,这种情况经常会破坏程序正常的流程。它通常由特殊的编程语言结构、计算机硬體机制(如:中断或者如訊號等操作系统IPC设施)所构成的。具体实现由硬體和軟體自身定义而决定。一些例外,尤其是硬體,将会在被中断后进行恢復。
硬體领域编辑
此章节需要扩充。 |
硬體的异常处理机制由 CPU 完成。这种机制支持错误检测,在发生错误后会将程序流跳转到专门的错误处理过程(英語:error handling routines)中。发生异常前的状态存储在栈上。[2]
操作系统提供的例外處理设施编辑
此章节需要扩充。 |
针对程序中可能发生的例外,操作系统可能通过 IPC 来提供对应的处理设施。进程执行过程中发生的中断通常由操作提供的「中断服务子程序」处理,操作系统可以藉此向该进程发送信号。进程可以通过注册信号处理器的方式自行处理信号,也可以让操作系统执行預設行为(比如终止该程序)。
从进程的视角,硬體中断相当于可恢复异常,虽然中断一般与程序流本身无关。
軟體领域编辑
在编程语言领域,通常 例外(英語:exception)这一术语所描述的是一种資料结构,该資料结构可以存储异常(exceptional)相关訊息。例外处理的常见的一种机制是移交控制权。引发(Raise)异常,也叫作抛(Throw)异常,通过该方式达到移交控制权的效果。例外抛出后,控制权会被移交至某处的接(Catch),并执行处理。
从子程序routine作者的角度看,如果要表示当前子程序无法正常执行,抛出例外是很好的选择。无法正常执行的原因可以是输入参数无效(比如值在函数的定义域之外),也可以是无法获得所需的资源(比如文件不存在、硬碟出错、内存不足)等等。在不支持例外的系统中,子程序需要通过返回特殊的错误码實作类似的功能。然而回傳错误码可能导致不完全预测问题,子程序的使用方需要编写额外的代码,才能将普通的回傳值与错误码相区别。
编程语言对异常有着截然不同的定义,但现代语言大致上可分两类:[3]
- 用作于控制流程的异常,如:Ada,Java,Modula-3,ML,OCaml,Python 和 Ruby 。
- 用作于处理不正常、无法预测、错误性的情况。如:C++,[4] C#,Common Lisp,Eiffel 和 Modula-2 。
Kiniry 强调“语言的设计仅仅部分地影响了例外机制的使用,结果上,(在整个系统的运行期间)形成的对异常使用的态度会处理影响部分或者所有的失败(错误)。另外,其他主要的影响还有示例、核心代码的编写、技术书籍杂志文章以及相关讨论”。[5]
历史编辑
在1960和1970年代,Lisp语言发展出軟體例外。最初版本是在1962年 Lisp 1.5的时候,这时候异常通过ERRSET
关键词进行捕捉,并在出错时候,通过NIL
进行回傳,而不是以前的终止程序或者进行调试器。[6]1960年代后半,MacLisp语言通过ERR
关键词引入引发(Raise)错误机制。[6]Lisp的这种创新不仅仅被应用于抛出错误,还被应用于非本地控制流(non-local control flow)。在在1972年6月,MacLisp 语言通过CATCH
和THROW
两个新的关键词来实现非本地控制流,并保留ERRSET
和 ERR
专门做错误处理。在1970中后,NIL
衍生清除(Cleanup)操作(LISP的新功能),对应着现今常见的finally
。[7]该操作也被 Common Lisp使用了。与之同时代,Scheme也诞生了dynamic-wind
,用于处理closures中的异常。Goodenough (1975a) and Goodenough (1975b)是首篇文章介绍结构化的异常处理。[8] 1980年后,异常处理被廣泛利用于许多编程语言。
PL/I语言使用的是动态域(Dynamically scoped)例外,然而稍微现代的编程语言多用词法作用域(lexically scoped的例外。PL/I语言的例外处理包含事件(不是错误)、注意(Attention)、EOF、列举了的变量的修改(Modification of listed variables)。虽然现在的一些编程语言支持不含错误信息的例外,但是他们并不常见。
一开始,軟體的例外处理是包含恢复的例外:恢复语法(Resumption semantics),就像大部分的硬體例外一样,以及不恢复的例外:终止语法(Termination semantics )。但是,在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
语句),会在这段代码的控制权结束时自动释放资源。
错误处理编辑
错误处理(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 exceptions)编辑
如果一个异常抛出后,没有被捕捉,那么未捕捉异常将会在运行时被处理。进行该处理的程序(routine)叫 未捕捉异常处理器(uncaught exception handler)[17][18]。大部分的处理是终止程序并将错误信息打印至控制台,该信息通常包含调试用(debug)的信息,如:异常的描述信息、栈追踪(stack trace)。[19][20][21]通常处于最高级(应用级别)的处理器,即便捕捉到异常也会避免终止自身(如:线程出现异常,主线程也不会终止)。[22][23]
值得了解的是,在即便未捕捉异常导致了程序异常中断(如:异常没被捕捉、滚动未完成、没释放资源),程序仍旧能正常地顺序性地关闭。只要确保运行时(runtime)能正常地运行,因为 运行时 控制着整个程序的执行。
作为默认的未捕捉异常处理器是可以被替换的,不管是全局还是单线程的,新的未捕捉异常处理器可以尝试做这些事情:未捕捉异常导致关闭了的线程,使之重启;提供另一种方式记录日志;让用户报告未捕捉异常等等。在Java中,单一线程可以使用Thread.setUncaughtExceptionHandler
,全局可以用Thread.setDefaultUncaughtExceptionHandler
;在python中,可通过修改sys.excepthook
。
异常的静态检查(Static checking of exceptions)编辑
此章节需要扩充。 |
检查性异常(Checked exceptions)编辑
Java的设计者设计了[24] 检查性异常(Checked exceptions)[25]。当方法引发“检查性异常”时,“检查性异常”将成为方法符号的一部分。例如:如果方法抛出了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)。语法错误是在运行之前发生的。而异常是在运行时发生的错误,它将无条件停止程序,除非进行捕捉处理。[26]
Java编辑
异常是异常事件(exceptional event)的缩写。异常是一个事件,它发生在程序运行时并会打乱程序指示的正常流程。当方法出现了错误时,方法会创建一个对象并将它交给运行时系统(runtime system),所创建的对象叫 异常对象(exception object),该对象包含了错误的信息(描述了出错时的程序的类型和状态)。创建错误对象和转交给运行时系统的过程,叫 抛出异常(throwing an exception)。[27]
class RuntimeException
和class Error
均是不检查的异常(Unchecked Exceptions)。[28]错误不等于错误类(class Error
),错误类代表着不应该被捕捉的严重的问题。[29]class RuntimeException
意味着程序出现问题了。[28]
Go编辑
Go语言提倡的是错误处理(error handling)。Go语言设计者系统希望使用者在错误出时,显式地检查错误。[30] Go虽然不提供与Java语言的try..catch
同等的功能语句,但是取而代之,提供了轻型的异常处理机制panic...recover
。[31]
异常安全编辑
一段代码是异常安全的,如果这段代码运行时的失败不会产生有害后果,如内存泄露、存储数据混淆、或无效的输出。异常安全可分成不同层次:
- 失败透明(failure transparency),也称作不抛出保证(no throw guarantee):代码的运行保证能成功并满足所有的约束条件,即使存在异常情况。如果出现了异常,将不会对外进一步抛出该异常。(异常安全的最好的层次)
- 提交或卷回的语义(commit or rollback semantics),或称作强异常安全(strong exception safety)或无变化保证(no-change guarantee):运行可以是失败,但失败的运行保证不会有负效应,因此所有涉及的数据都保持代码运行前的初始值。[32]
- 基本异常安全(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
并未实际插入,使得内部状态不一致。对于“异常不安全”的实现,程序可能会崩溃,例如写入数据到无效的内存。
通常至少需要基本异常安全。失败透明是难于实现的,特别是在编写库函数时,因为对应用程序的复杂知识缺少获知。
参考文献编辑
- ^ abnormality汉语(繁体)翻译:剑桥词典. dictionary.cambridge.org. [2020-02-04]. (原始内容存档于2021-04-14) (中文(简体)).
- ^ Hardware Exceptions Detection. TEXAS INSTRUMENTS. 2011-11-24 [2012-10-05]. (原始内容存档于2013-11-10) (英语).
- ^ 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.
- ^ Stroustrup: C++ Style and Technique FAQ. www.stroustrup.com. [5 May 2018]. (原始内容存档于2 February 2018).
- ^ 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.0 6.1 Gabriel & Steele 2008,第3頁.
- ^ White 1979,第194頁.
- ^ Stroustrup 1994,第392頁.
- ^ Stroustrup 1994,16.6 Exception Handling: Resumption vs. Termination, pp. 390–393.
- ^ C.A.R. Hoare. "The Emperor's Old Clothes". 1980 Turing Award Lecture
- ^ Weimer, W; Necula, G.C. Exceptional Situations and Program Reliability (PDF) 30 (2). 2008. (原始内容存档 (PDF)于2015-09-23).
|journal=
被忽略 (帮助) - ^ 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.
- ^ Panic And Recover 互联网档案馆的存檔,存档日期2013-10-24., Go wiki
- ^ Weekly Snapshot History. golang.org. (原始内容存档于2017-04-03).
- ^ Proposal for an exception-like mechanism. golang-nuts. 25 March 2010 [25 March 2010]. (原始内容存档于2013-03-06).
- ^ Effective Go. golang.org. (原始内容存档于2015-01-06).
- ^ Mac Developer Library, "Uncaught Exceptions 互联网档案馆的存檔,存档日期2016-03-04."
- ^ MSDN, AppDomain.UnhandledException Event 互联网档案馆的存檔,存档日期2016-03-04.
- ^ Mac Developer Library, "Uncaught Exceptions 互联网档案馆的存檔,存档日期2016-03-04."
- ^ The Python Tutorial, "8. Errors and Exceptions 互联网档案馆的存檔,存档日期2015-09-01."
- ^ Java Practices -> Provide an uncaught exception handler. www.javapractices.com. [5 May 2018]. (原始内容存档于9 September 2016).
- ^ Mac Developer Library, "Uncaught Exceptions 互联网档案馆的存檔,存档日期2016-03-04."
- ^ Exception Handling — PyMOTW 3. pymotw.com. [2020-02-03]. (原始内容存档于2021-05-16).
- ^ Google Answers: The origin of checked exceptions. [2011-12-15]. (原始内容存档于2011-08-06).
- ^ Java Language Specification, chapter 11.2. http://java.sun.com/docs/books/jls/third_edition/html/exceptions.html#11.2 互联网档案馆的存檔,存档日期2006-12-08.
- ^ 8. Errors and Exceptions — Python 3.8.1 documentation. docs.python.org. [2020-02-04]. (原始内容存档于2022-06-08).
- ^ What Is an Exception? (The Java™ Tutorials > Essential Classes > Exceptions). docs.oracle.com. [2020-02-04]. (原始内容存档于2022-06-09).
- ^ 28.0 28.1 Unchecked Exceptions — The Controversy (The Java™ Tutorials > Essential Classes > Exceptions). docs.oracle.com. [2020-02-04]. (原始内容存档于2022-06-07).
- ^ Error (Java Platform SE 8 ). docs.oracle.com. [2020-02-04]. (原始内容存档于2021-10-24).
- ^ Error handling and Go - The Go Blog. blog.golang.org. [2020-02-04]. (原始内容存档于2021-07-12).
- ^ Google 网上论坛. groups.google.com. [2020-02-04]. (原始内容存档于2011-01-22).
- ^ 存档副本. [2011-08-13]. (原始内容存档于2009-02-03).