DELPHI学习--,异常

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。