在计算机编程中,一些编程语言提供了用于例外处理(,或意译为异常处理)的机制。例外()这一术语所描述的通常是一种資料结构,该資料结构可以存储例外的相关訊息。例外处理的常见的一种机制是移交控制权。引发(raise)例外,也叫作抛出(throw)例外,通过该方式达到移交控制权的效果。例外抛出后,控制权会被移交至某处的接住(catch),并执行处理。
概述
从子程序作者的角度看,如果要表示当前子程序无法正常执行,抛出例外是很好的选择。无法正常执行的原因可以是输入参数无效(比如值在函数的定义域之外),也可以是无法获得所需的资源(比如文件不存在、硬碟出错、内存不足)等等。在不支持例外的系统中,子程序需要通过返回特殊的實作类似的功能。然而回傳错误码可能导致,子程序的使用方需要编写额外的代码,才能将普通的回傳值与错误码相区别。
编程语言在例外是什么的观念上有着微妙的差异。例外可以被用来表示并处理异常(abnormal)、不可预测(unpredictable)、错误(erroneous)的情况,还可以用作流程控制结构来处理正常的情况。例如,Python迭代器抛出StopIteration例外来指示这个迭代器没有项目可以产生了。在很多语言中,对例外的惯常用法该怎样构成的存在分歧。例如,Joshua Bloch声称Java的例外处理应当只用于异常情况,但是Kiniry观察到Java的类根本就不是异常事件。类似的,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中后,(“新实现的LISP”)衍生出清除操作UNWIND-PROTECT,对应着现今常见的finally。该操作也被Common Lisp使用了。与之同时Scheme也提供了对续体调用进行“保护”的dynamic-wind,只要控制进入或离开它所界定范围包括通过非局部跳转方式都会分别执行特定的任务。和是介绍结构化的异常处理的开创性文章。 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惯用法会混淆控制流,并介入了类似异常的/机制。不同于之处在于它只能在一个函数中的代码块之内调用,所以处理器只能做清洁和变更这个函数的返回值,而不能将控制返回到这个函数内的任意点。块自身功能类似于子句。
Rust语言没有例外,它使用(一种)来处理运行时错误,而对于严重错误使用宏。
编程语言相关支持
许多常见的程序设计语言支持异常处理,包括:
*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中进行异常处理的条件系统,不采取-{zh-hans:栈解开;zh-hant:堆疊輾轉開解;}-,因此允许异常处理完后在抛出异常的代码处原地恢复执行。
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 意味着程序出现问题了。。大部分的处理是终止程序并将错误信息打印至控制台,该信息通常包含调试用的信息,如:异常的描述信息、栈追踪。通常处于最高级(应用级别)的处理器,即便捕捉到异常也会避免终止自身(如:线程出现异常,主线程也不会终止)。
值得了解的是,在即便未捕捉异常导致了程序异常中断(如:异常没被捕捉、滚动未完成、没释放资源),程序仍旧能正常地顺序性地关闭。只要确保运行时系统能正常地运行,因为运行时系统控制着整个程序的执行。
作为默认的未捕捉异常处理器是可以被替换的,不管是全局还是单线程的,新的未捕捉异常处理器可以尝试做这些事情:未捕捉异常导致关闭了的线程,使之重启;提供另一种方式记录日志;让用户报告未捕捉异常等等。在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 "[https://db.usenix.org/events/wiess2000/full_papers/dinechin/dinechin.pdf C++ Exception Handling] " by Christophe de Dinechin
- Article "[http://java.sun.com/developer/technicalArticles/Programming/exceptions2/index.html Exceptional practices] " by Brian Goetz
- Article "[http://perl.com/pub/a/2002/11/14/exception.html Object Oriented Exception Handling in Perl] " by Arun Udaya Shankar
- Article "[http://oreillynet.com/pub/a/network/2003/05/05/cpluspocketref.html Programming with Exceptions in C++] " by Kyle Loudon
- Article "[http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html Unchecked Exceptions - The Controversy] "
- [http://c2.com/cgi/wiki?CategoryException Descriptions from Portland Pattern Repository]
- [https://web.archive.org/web/20020405175011/http://www.mindview.net/Etc/Discussions/CheckedExceptions Does Java Need Checked Exceptions?]
评论 (0)