Exceptions(异常)
Exceptions: Overview(概述)
当发生错误或其它事件而打断了程序的正常执行时,将引发一个异常。异常把控制权交给一个异常处理
程序(exception handler),这使我们把错误处理和正常的程序逻辑隔离开来。因为异常属于对象,我们
可以应用继承关系把它们分层组织,在不影响现有代码的情况下能引入新的异常。异常能传送一些信息
(比如错误消息),把它们从异常发生点带到被处理的地方。
当程序使用SysUtils 单元时,所有的运行时错误都将被转换为异常,否则,像内存不足、被零除、GPF
(general protection fault)等错误会终止程序,而现在它们能被捕获并进行处理。
When to use exceptions(何时使用异常)
异常提供了一种优雅的方式来捕获运行时错误,而不是挂起程序和使用笨拙的条件语句。但是,Object
Pascal 异常处理机制的复杂性降低了它的效率,所以应当酌情使用。虽然(几乎)能以任何原因引发一
个异常,也可以把(几乎)任何代码段使用try...except 或try...finally 封装起来进行保护,但实际上最好
把它们用在特殊情况。
异常处理程序适用于以下几种情况:发生几率比较低或难以预料、但结果却是灾难性(比如程序崩溃)
的错误;对于if...then 语句来说,错误条件非常复杂或难以判断;当需要响应操作系统引发的异常,或
一些你不能得到源码而又必须对它们的异常做出响应的例程。异常通常用在硬件、内存、I/O 和操作系
统错误。
条件语句经常是判断错误的最好方式。比如,假设你要在打开一个文件之前先确定它是否存在,你以下
面的方式实现它:
try
AssignFile(F, FileName);
Reset(F); // 若没有发现文件则引发一个EinOutError 异常
except
on Exception do ...
end;
但你也可以使用下面的方式来避免异常处理的开销
if FileExists(FileName) then // 若没有发现文件则返回False,不会引发异常
begin
AssignFile(F, FileName);
Reset(F);
end;
Assertions 提供了另一种方式,使你在源代码的任何地方判断一个布尔条件。当一个Assert 语句失败时,
程序或者挂起,或者引发一个EAssertionFailed 异常(若它使用SysUtils 单元的话)。只有当判断一个你
不期望发生的条件时,你才应该使用Assertions。要了解更多信息,请参考在线帮助中的the standard
procedure Assert。
Declaring exception types(声明异常类型)
异常类的声明和其它类一样,实际上,使用任何类的一个实例表示异常是可行的,但推荐从SysUtils 单
元的Exception 类进行派生。
你能应用继承关系给异常分组,比如,下面是SysUtils 单元中的声明,它为计算错误定义了一组异常类
type
EMathError = class(Exception);
EInvalidOp = class(EMathError);
EZeroDivide = class(EMathError);
EOverflow = class(EMathError);
EUnderflow = class(EMathError);
给定上面的声明,你能定义一个单一的EMathError 异常处理程序,它也能处理EInvalidOp、EZeroDivide、
EOverflow 和EUnderflow 异常。
有时,异常类会定义字段、方法和属性,它们用来传达一些额外的错误信息。比如,
type EInOutError = class(Exception)
ErrorCode: Integer;
end;
Raising and handling exceptions(引发和处理异常)
Raising and handling exceptions(引发和处理异常)
要创建一个异常对象,在raise 语句中调用异常类的构造函数。比如,
raise EMathError.Create;
通常,raise 语句的格式是
raise object at address
这里,object 和at address 都是可选的。若省略了object,则语句重新引发当前异常,请参考Re-raising
exceptions;当指定了一个地址,它通常是指向过程或函数的指针,使用这个选项,可使异常从堆栈中一
个较早点引发,而不是从它实际发生的地点引发(use this option to raise the exception from an earlier point
in the stack than the one where the error actually occurred)。
当引发一个异常时,也就是使用了raise 语句(referenced in a raise statement),它将受异常处理逻辑的控
制。一个raise 语句永远不会以正常方式返回控制,相反,它把控制权交给能处理指定的异常(类)、并
且在最内层的异常处理程序(最内层是指最后进入但还没有退出的一个try...except 块)。
比如,下面的函数把一个字符串转换为整数,若结果超出指定的范围则引发一个ERangeError 异常。
function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
Result := StrToInt(S); // StrToInt 在SysUtils 单元声明
if (Result < Min) or (Result > Max) then
raise ERangeError.CreateFmt(
'%d is not within the valid range of %d..%d',
[Result, Min, Max]);
end;
注意raise 语句中调用的CreateFmt 方法。Exception 和它的派生类有特殊的构造函数,提供了可选择的
方法来创建异常消息和context ID。
被引发的(raised)异常在处理后自动清除,永远不要试图手动销毁它。
注意:在单元的初始化部分引发一个异常可能无法产生预期的结果。对异常的正规支持来自SysUtils 单
元,在获得这种支持之前,它必须被初始化。如果在初始化期间产生了异常,所有被初始化的单元(包
括SysUtils)执行结束化处理,并重新引发异常。然后,通常是结束程序来捕获和处理异常(Then the
exception is caught and handled, usually by interrupting the program)。
Try … except statements(Try … except 语句)
异常在try...except 语句中被处理,比如,
try
X := Y/Z;
except
on EZeroDivide do HandleZeroDivide;
end;
上面的语句尝试Y 被Z 除,若EZeroDivide 异常发生,则调用例程HandleZeroDivide。
try...except 语句的语法是
try statements except exceptionBlock end
这里,statements 是语句序列(由分号隔开的一系列语句),exceptionBlock 或者是
• 其它语句序列,或者是
• 一系列异常处理程序,后面跟可选的
else statements
一个异常处理程序具有如下格式
on identifier: type do statement
这里,identifier:是可选的(若有的话,它可以是任何有效标志符),type 用来表示异常类,statement 是任
何语句。
一个try...except 语句执行开始处的(初始)代码,若没有引发异常,异常代码段(exceptionBlock)被忽
略,程序控制转到下一部分。
若执行初始代码时发生了异常(或者执行了raise 语句,或者是调用过程或函数引起的),都将试图对它
进行处理:
• 若异常处理块(exception block)中有对应的异常,则控制权交给第一个匹配的处理程序。当处理程
序中指定的异常类和(发生的)异常所属的类相同,或者是异常的祖先类时,我们说,这个异常处
理程序与这个异常相“匹配”。
• 若没有发现相应的异常处理程序,当有else 子句时,程序控制转到else 子句。
• 若异常处理块中没有异常处理程序,而只是语句序列,则程序控制转到它的第一个语句。
如果上面的条件都不成立,会继续搜索下一个try...except 语句块;若还没有合适的异常处理程序、或else
子句或语句序列,搜索会继续扩展到下一个try...except 语句块,依此类推。如果达到最外层的try...except
语句块并且异常还没有被处理,程序就会终止。
当处理一个异常时,堆栈退回到包含try...except 语句的过程或函数,程序控制权转给异常处理程序、else
子句或语句序列。这个过程忽略所有进入try...except 后调用的过程和函数,然后,异常对象自动调用析
构函数进行销毁,程序控制权转给try...except 后面的语句。(如果调用Exit、Break 或Continue 使程序控
制权离开了异常处理程序,异常对象也会自动销毁。)
在下面的例子中,第1 个异常处理程序处理被0 除异常,第2 个处理溢出,最后一个处理其它的数学运
算异常。EMathError 在最后出现,因为它是另外两个异常的祖先,若它最先出现,另外两个将永远不会
被调用。
try
...
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
end;
在异常处理程序中,可以在异常类之前指定一个标志符,在执行on...do 后面的语句时,它表示异常对象,
标志符的作用域被限定在这个语句中。比如,
try
...
except
on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;
若在异常处理块中使用了else 子句,则它处理所有未经异常处理程序处理的异常。比如,
try
...
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
else
HandleAllOthers;
end;
这里,else 子句处理所有不是EMathError 的异常。
若异常处理块没有异常处理程序,而只是包含一系列语句,则它们处理所有的异常。比如,
try
...
except
HandleException;
end;
这里,try 和except 之间的代码在运行时产生的异常,都由HandleException 例程进行处理。
Re-raising exceptions(重新引发一个异常)
当关键字raise 在异常块中出现,并且它的后面没有对象引用时,它引发正在处理的异常。这使得异常
处理程序能对错误做有限处理后重新引发它。对于发生异常后必须进行清除工作、但又不能进行全面处
理的过程或函数,重新引发一个异常是有用的。
比如,GetFileList 函数分配一个TStringList 对象,并用指定搜索路径下的文件名来填充它。
function GetFileList(const Path: string): TStringList;
var
I: Integer;
SearchRec: TSearchRec;
begin
Result := TStringList.Create;
try
I := FindFirst(Path, 0, SearchRec);
while I = 0 do
begin
Result.Add(SearchRec.Name);
I := FindNext(SearchRec);
end;
except
Result.Free;
raise;
end;
end;
GetFileList 创建一个TStringList 对象,然后使用FindFirst 和FindNext 函数(在SysUtils 单元定义)来初
始化它。如果初始化失败(比如搜索路径无效,或者没有足够的内存来填充字符串列表),GetFileList
需要释放字符串列表,因为函数的调用者还不知道它的存在。由于这个原因,初始化字符串列表在
try...except 语句中执行,若发生了异常,异常处理块释放字符串列表,然后重新引发这个异常。
Nested exceptions(嵌套的异常)
对于异常处理程序,它自己也可以引发和处理异常。只要这些异常也是在异常处理程序的内部被处理,
它们并不影响原来的异常;但是,若它超越了异常处理程序,原来的异常就会丢失。下面的Tan 函数说
明了这一点。
type
ETrigError = class(EMathError);
function Tan(X: Extended): Extended;
begin
try
Result := Sin(X) / Cos(X);
except
on EMathError do
raise ETrigError.Create('Invalid argument to Tan');
end;
end;
若Tan 在执行过程中发生了EMathError 异常,则异常处理程序引发一个ETrigError 异常。因为Tan 没有
为ETrigError 提异常供处理程序,异常就传播到原异常处理程序的外面,从而导致EMathError 被销毁。
对于函数调用者来说,就像Tan 函数引发了一个ETrigError 异常。(不明白)
Try … finally statements(Try … finally 语句)
有时,我们希望不管有没有发生异常,指定的一部分操作都要被完全执行。比如,当一个例程需要控制
一个资源,不管例程是否正常结束,能释放资源是非常重要的。在这种情况下,你可以使用try...finally
语句。
下面的例子演示这段代码如何打开和处理一个文件,并且,即使在执行过程中发生了错误也能保证在最
后关闭文件。
Reset(F);
try
... // 处理文件F
finally
CloseFile(F);
end;
Classes and objects
- 121 -
try...finally 语句的语法是
try statementList1 finally statementList2 end
这里,每个statementList 是一系列由分号隔开的语句。try...finally 语句执行statementList1(try 子句)中
的命令,若它执行完毕并没有引发异常,statementList2(finally 子句)被执行。若在执行statementList1
时发生了异常,程序控制权转给statementList2,一旦它执行完毕,异常被重新引发。即使调用Exit、Break
或Continue 过程使程序控制权离开了statementList1,statementList2 也会自动执行。所以,不论try 子句
如何结束,finally 子句总是被执行。
若异常发生了但却没有在finally 子句中进行处理,异常会传播到try...finally 语句的外面,这样,在try
子句中已经引发的异常都会丢失。所以,finally 子句应当处理所有本地引发的异常,这样就不会打乱其
它异常的传播。
Standard exception classes and routines(标准异常类和例程)
SysUtils 单元声明了几个标准例程来处理异常,它们包括ExceptObject、ExceptAddr 以及ShowException。
SysUtils 和其它单元还包括很多异常类,它们(除了OutlineError)都是从Exception 派生而来。
Exception 类有Message 和HelpContext 的属性,它们用于传递错误描述和context ID,后者用于上下文相
关联机文档;它还定义了多个构造函数,使你能以不同的方式指定描述信息和context ID。