注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

yeye55的博客

编程就象品一杯清茶,清淡却又深厚。

 
 
 

日志

 
 
关于我

宅男一只,喜欢编程。没事的时候就喜欢抱着电脑看看电影、看看动漫、听听音乐、打打网游、逛逛论坛。清静的时候喜欢专研编程技术,但是发现自己至今仍然只是一只三脚猫。

网易考拉推荐

Delphi XE4 语言指南 - 12 异常  

2014-11-25 20:02:54|  分类: XE4参考指南 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

  本指南名称:Delphi XE4 语言指南,作者:叶叶,于2014年11月25日在网易博客上发表。页面地址:http://yeye55blog.blog.163.com/blog/static/197241021201410252936156/ 。本指南全文可以在上述页面中下载。请尊重他人劳动成果,转载或引用时请注明出处。

12 异常

  如果发生了一个错误或者是其他事件打断了一个程序的正常执行,那么就会引发一个异常(exception)。异常会将控制权转移到一个异常处理块(exception handler)中。利用异常处理可以将正常的程序逻辑与错误处理分开。因为异常是对象,所以它们可以利用继承来分为不同的层次,并且可以引入新的异常而不影响现有的代码。异常可以携带信息,例如错误消息,并且可以把这些信息从它的引发点传递到它的处理块中。

  如果一个应用程序引用了System.SysUtils单元,那么大多数的运行时错误都会被自动转换为异常。许多会终止应用程序的错误可以被捕获和处理,例如,内存不足、除数为零以及一般的保护性错误。

  异常提供了一种优雅的方式来捕获运行时错误,不需要停止程序的运行,也没有尴尬的条件语句。通过实施异常处理语义会要求增加一个代码/数据的大小并且会造成运行时的性能损失。虽然任何理由都有可能引发异常,并且可以将任何代码块放置在try...except语句或try...finally语句中对其进行保护,但是在实践中,这些工具最好留给特殊情况使用。

  异常处理适合那些发生率较低或者是难以评估但其后果可能是灾难性(例如应用程序崩溃)的错误,该错误的条件是很复杂的或者是很难用if...then语句来进行测试。对于操作系统引发的异常,或者是源代码无法控制的例程所引发的异常,都需要使用异常处理来对其做出反应。异常一般用于处理硬件错误、内存错误、输入输出错误和操作系统错误。条件语句通常是测试错误的最好方式,可以通过使用条件语句来避免异常处理的开销。例如下面的这两段代码。

01 //如果文件不存在则返回False,不会引发异常。
02 if FileExists(FileName) then
03     F := TFileStream.Create(FileName, fmOpenRead)
04 else
05     ShowMessage('文件' + FileName + '不存在!');
01 //如果文件不存在则会引发一个EInOutError异常。
02 try
03     F := TFileStream.Create(FileName, fmOpenRead);
04 except
05     on e : EInOutError do ShowMessage(e.Message);
06 end;

代码12.1 条件语句测试与异常处理

  在代码12.1中,在试图打开一个文件时需要确保该文件是存在的。第一段代码使用了条件语句进行测试,不会引发异常,而第二段代码使用了异常来进行处理。

  过程Assert提供了另外一种方式来测试一个Boolean类型的表达式,该过程可以在源代码的任意位置中进行调用。如果过程Assert测试的表达式为False,那么程序会因一个运行时错误而被停止,或者是(如果引用了System.SysUtils单元)引发一个EAssertionFailed异常。过程Assert应该只用于对那些不希望发生的条件进行测试。

  异常类型就是类类型,异常就是对象。可以使用raise语句来引发一个异常。可以使用try...except语句来捕获并处理一个异常。可以使用try...finally语句来确保一个必须要进行的操作不会因为异常的发生而被忽略。

12.1 异常声明

  异常类型可以像其他类一样进行声明。实际上,可以把任意类的实例作为异常来使用。但是建议从System.SysUtils单元中声明的Exception类中进行派生。可以利用继承将异常分类到不同的家族中。例如,在System.SysUtils单元中定义了一个用于表示数学错误的异常类型的家族,这些异常类型的声明如下所示。

01 type
02     EMathError = class(Exception);
03     EInvalidOp = class(EMathError);
04     EZeroDivide = class(EMathError);
05     EOverflow = class(EMathError);
06     EUnderflow = class(EMathError);

代码12.2 异常类型家族

  对于代码12.2中的声明,可以定义一个EMathError异常处理块来处理EInvalidOp、EZeroDivide、EOverflow和EUnderflow全部这些异常。在异常类中可以定义字段、方法或属性,用来传递关于错误的额外信息。例如下面的这个声明。

01 type
02     EInOutError = class(Exception)
03         ErrorCode : Integer;
04     end;

代码12.3 在异常类中定义字段

  在System单元和System.SysUtils单元中声明了几个标准例程用来对异常进行处理,包括了ExceptObject、ExceptAddr、ShowException和ExceptionErrorMessage。在System单元、System.SysUtils单元、以及其他的单元中还声明了几十个异常类。这些异常类除了一部分派生于OutlineError类,其他的都派生于Exception类。Exception类直接派生于TObject类,它的成员如下表所示。

成员 类别 可见性 说明
BaseException 属性 公开 指定最内层的异常。
CleanUpStackInfoProc 字段 公开 清理栈信息。
Create 构造 公开 使用一个简单消息字符串来创建一个异常类的实例。
CreateFmt 构造 公开 使用一个格式化消息字符串来创建一个异常类的实例。
CreateFmtHelp 构造 公开 使用一个格式化消息字符串和一个帮助上下文ID来创建一个异常类的实例。
CreateHelp 构造 公开 使用一个简单消息字符串和一个帮助上下文ID来创建一个异常类的实例。
CreateRes 构造 公开 使用一个从应用程序的资源中导入的简单消息字符串来创建一个异常类的实例。
CreateResFmt 构造 公开 使用一个从应用程序的资源中导入的格式化消息字符串来创建一个异常类的实例。
CreateResFmtHelp 构造 公开 使用一个从应用程序的资源中导入的格式化消息字符串和一个帮助上下文ID来创建一个异常类的实例。
CreateResHelp 构造 公开 使用一个从应用程序的资源中导入的简单消息字符串和一个帮助上下文ID来创建一个异常类的实例。
Destroy 析构 公开 销毁一个异常对象实例。
GetBaseException 函数 公开 返回最内层的异常。
GetExceptionStackInfoProc 字段 公开 从一个异常记录中生成栈信息。
GetStackInfoStringProc 字段 公开 生成一个栈跟踪的格式化字符串。
GetStackTrace 函数 保护 返回在异常被引发时的栈跟踪。
HelpContext 属性 公开 表示与异常相关联的上下文敏感的联机帮助的上下文ID编号。
InnerException 属性 公开 指定内层异常。
Message 属性 公开 包含一个在异常被引发时需要在异常对话框中显示的文本字符串。
RaiseOuterException 过程 公开 引发一个新的异常,并保留当前的异常。
RaisingException 过程 保护 准备异常信息。
SetInnerException 过程 保护 设置内层异常。
SetStackInfo 过程 保护 存储栈信息。
StackInfo 属性 公开 当这个异常被引发时,提供栈信息。
StackTrace 属性 公开 指定在异常被引发时的栈跟踪。
ThrowOuterException 过程 公开 引发一个新的异常,并保留当前的异常。
ToString 函数 公开 将当前异常以及所有的内层异常的Message
属性合并成一个字符串并返回。

表12.1 Exception类的成员(不含继承)

  Exception类中的Message属性可以被用来传递一个错误的描述,HelpContext属性可以被用来传递一个上下文相关的联机文档中的上下文ID。Exception类还定义了各种构造方法可以使用不同的方式来指定错误描述和上下文ID。

12.2 raise语句

  要引发一个异常对象,需要使用一个带有异常类的实例的raise语句。raise语句的语法格式如下所示。

raise 对象 at 地址

  其中的“对象”和“at 地址”都是可选的。可以直接在raise语句中创建一个异常对象。例如下面的这条语句。

raise EMathError.Create;

  如果在raise语句中指定了一个地址,那么该地址可以是任意指针类型的表达式,不过通常该地址是一个指向过程或函数的指针。例如下面的这条语句。

raise Exception.Create('消息参数') at @MyFunction;

  使用“地址”选项可以在比错误实际发生更早的一个在栈中的位置上引发异常。如果一个异常被引发,即,在raise语句中引用了该异常,那么它将由一个特殊的异常处理逻辑来进行管理。在正常方式下raise语句从来不会将控制权返回。相反,它会将控制权转移到能够处理给定类的异常的最内层的异常处理块中。最内层的处理块是一个最近进入但尚未退出的try...except块。一个引发的异常在被处理之后会被自动销毁。永远不要试图手动销毁引发的异常。一个引发异常的例子如下所示。

01 function StrToIntRange(const S : String; Min, Max : Longint) : Longint;
02 begin
03     Result := StrToInt(S);
04     if (Result < Min) or (Result > Max) then
05         raise ERangeError.CreateFmt(
06             '%d不在有效范围%d至%d内', [Result, Min, Max]);
07 end;

代码12.4 引发异常的例子

  在代码12.4中定义的函数中,将一个字符串转换为一个整数,如果该整数超出了指定的范围,那么该函数就会引发一个ERangeError异常。注意,在raise语句中调用的CreateFmt方法是Exception类及其子类中的一个特殊的构造方法。除了使用消息和上下文ID来创建异常的方式之外,CreateFmt方法提供了另外一种方式。

  注意,在单元的初始化部分中引发一个异常可能不会产生预期的结果。正常的异常支持来自System.SysUtils单元,在初始化之前这种支持必须是有效的。如果在初始化期间发生了异常,那么所有已初始化的单元(包括System.SysUtils单元)都会被终止并且重新引发异常。然后,通常是由中断程序来捕获和处理这个异常。同样的,在单元的终止化部分中引发一个异常时,如果是在System.SysUtils单元已经被终止之后才引发的,那么该异常可能不会产生预期的结果。

12.3 try...except语句

  异常的处理是在try...except语句中进行的。try...except语句的语法格式如下所示。

try
    语句序列
except
    异常块
end

  其中,在保留字try之后的是一个语句序列,该语句序列中的语句之间必须使用分号“;”进行分隔。在保留字except之前的最后一个分号是可以被省略的。其中的异常块可以是另外一个语句序列或者是一个异常处理块列表。异常处理块列表的语法格式如下所示。

on 标识符1 : 类型1 do 语句1;
on 标识符2 : 类型2 do 语句2;
...
on 标识符n : 类型n do 语句n;
else 语句n + 1;

  其中的“标识符 :”是可选的。但是如果包含,那么其中的标识符必须是一个有效的标识符。其中的类型用来表示异常类型。其中的语句可以是任意语句。其中的else子句是可选的。一个使用try...except语句的例子如下所示。

01 try
02     X := Y / Z;
03 except
04     on EZeroDivide do HandleZeroDivide;
05 end;

代码12.5 try...except语句的使用

  在代码12.5中试图将Y除以Z,如果Z为零从而引发了EZeroDivide异常,那么就调用名为HandleZeroDivide的例程来进行处理。

  在执行try...except语句时,会先执行保留字try之后的语句序列。如果没有引发异常,那么异常块就会被忽略,并且将控制权传递给程序的下一个部分。如果在执行语句序列的过程中引发了一个异常,那么不管该异常是在该语句序列中执行raise语句引发的,还是在该语句序列中调用过程或函数时引发的,都会使用以下步骤来尝试处理该异常。

  一、如果在异常块中有任意一个处理块可以匹配该异常,那么控制权就会被传递到第一个这样的处理块中。只有当处理块所指定的类型是该异常的类类型或者是其父类类型时,该异常处理块才会被认为是与该异常匹配的。

  二、如果没有找到这样的处理块,同时在异常块中有一个else子句,那么控制权就会被传递到else子句中。

  三、如果在异常块中只有一个语句序列没有其他的异常处理块,那么控制权就会被传递到该语句序列中的第一条语句上。

  如果上述条件都不满足,那么将会继续搜索下一个最近进入但尚未退出的try...except语句中的异常块。如果在该异常块中没有发现适合的处理块、else子句或语句序列,那么将会继续搜索再下一个最近进入但尚未退出的try...except语句中的异常块。以此类推。如果在到达最外层的try...except语句后该异常仍然没有被处理,那么程序就会被终止。

  在处理一个异常时,栈(stack)会被追溯到包含处理发生时所在的try...except语句的过程或函数中,并且控制权会被转移到执行异常处理块、else子句或语句序列中。在进入异常处理所在的try...except语句之后发生的所有的过程和函数的调用都将被丢弃。在异常处理完成之后,会通过调用异常对象的Destroy析构方法来自动销毁该异常对象,并且控制权会被传递到try...except语句随后的语句上。即使通过调用exit、break或continue标准过程来使控制权离开异常处理块,异常对象仍然会被自动销毁。一个使用异常处理块的例子如下所示。

01 try
02     //此处省略部分代码...
03 except
04     on EZeroDivide do HandleZeroDivide;
05     on EOverflow do HandleOverflow;
06     on EMathError do HandleMathError;
07 end;

代码12.6 异常处理块的使用

  在代码12.6中,第一个异常处理块用于处理除零异常,第二个用于处理溢出异常,最后一个用于处理其他的数学异常。将EMathError异常放置在异常块的最后面是因为它是其他两个异常类的父类,如果把它放置在第一个,那么其他的两个处理块永远都不会被调用。

  在异常处理块中可以在异常类名称之前指定一个标识符。这个声明的标识符可以在执行on...do随后语句的期间用于表示异常对象。该标识符的作用域仅局限于该语句之中。例如下面的这段代码。

01 try
02     //此处省略部分代码...
03 except
04     on E : Exception do ErrorDialog(E.Message, E.HelpContext);
05 end;

代码12.7 引用异常对象

  如果在异常块中指定了一个else子句,那么该else子句将处理那些不是由异常块中的其他异常处理块处理的异常。例如下面的这段代码。

01 try
02     //此处省略部分代码...
03 except
04     on EZeroDivide do HandleZeroDivide;
05     on EOverflow do HandleOverflow;
06     on EMathError do HandleMathError;
07     else HandleAllOthers;
08 end;

代码12.8 else子句的使用

  在代码12.8中,else子句将处理那些不是EMathError异常的其他任意异常。

  在异常块中可以不包含任何的异常处理块,只包含一个语句序列用来处理所有的异常。例如下面的这段代码。

01 try
02     //此处省略部分代码...
03 except
04     HandleException;
05 end;

代码12.9 不使用异常处理块

  在代码12.9中,在执行try与except之间的语句的过程中所发生的任何异常都将由例程HandleException来进行处理。

  如果保留字raise出现在一个异常块中其后没有跟随一个对象引用,那么它将会重新引发由该异常块处理的任意异常。这样可以允许一个异常处理块以有限的方式响应一个错误,然后再重新引发该异常。如果一个过程或函数在异常发生后需要进行一些清理但却无法完全处理该异常,那么这时重新引发异常就是非常有用的。例如下面的这段代码。

01 function GetFileList(const Path : string) : TStringList;
02 var
03     I : Integer;
04     SearchRec : TSearchRec;
05 begin
06     Result := TStringList.Create;
07     try
08         I := FindFirst(Path, 0, SearchRec);
09         while I = 0 do
10         begin
11             Result.Add(SearchRec.Name);
12             I := FindNext(SearchRec);
13         end;
14     except
15         Result.Free;
16         raise;
17     end;
18 end;

代码12.10 重新引发异常

  在代码12.10中,GetFileList函数创建了一个TStringList对象,并且从指定的搜索路径中查找匹配的文件名添加到该对象中。在GetFileList函数中使用FindFirst函数和FindNext函数(在System.SysUtils单元中声明)来进行操作。如果操作失败,例如搜索路径无效,或者是没有足够的内存来存储字符串列表,那么GetFileList函数需要销毁新建的TStringList对象,因为调用者还不知道它的存在。由于这个原因,所以TStringList对象的操作需要在try...except语句中进行的。如果发生了一个异常,那么该语句的异常块就会销毁该TStringList对象,然后重新引发该异常。

  在异常处理块所执行的代码本身中也可以引发和处理异常。由于这些异常也是在异常处理块的内部进行处理的,所以它们不会影响到原有的异常。但是,如果一个在异常处理块中引发的异常一旦被传播到了该处理块之外,那么原有的异常将会被丢弃。例如下面的这段代码。

01 type
02     ETrigError = class(EMathError);
03
04 function Tan(X : Extended) : Extended;
05 begin
06     try
07         Result := Sin(X) / Cos(X);
08     except
09         on EMathError do
10             raise ETrigError.Create('无效参数');
11     end;
12 end;

代码12.11 重新引发新的异常

  在代码12.11中,如果在执行Tan函数的过程中发生了一个EMathError异常,那么对应的异常处理块将会引发一个ETrigError异常。由于Tan函数没有为ETrigError异常提供一个处理块,所以该异常将从原有的异常处理块中传播出来,从而导致EMathError异常被销毁。对于调用者来说,这就好像Tan函数引发了一个ETrigError异常。

12.4 try...finally语句

  有时需要确保一个操作中的某些部分必须完成,而不管该操作是否会被异常打断。例如,当一个例程获取了一个资源的控制权时,通常最重要的就是释放该资源,而不管该例程是否会正常终止。在这种情况下可以使用try...finally语句来进行实现。try...finally语句的语法格式如下所示。

try
    语句序列1
finally
    语句序列2
end

  其中,在保留字try之后的是一个语句序列,在保留字finally之后的是另外一个语句序列,这些语句序列中的语句之间必须使用分号“;”进行分隔。但是,在保留字finally和end之前的最后一个分号是可以被省略的。一个使用try...finally语句的例子如下所示。

01 F := TFileStream.Create(FileName, fmOpenRead);
02 try
03     //此处省略部分代码...
04 finally
05     F.Free;
06 end;

代码12.12 try...finally语句的使用

  在代码12.12中打开了一个文件,并对其进行了一些操作。即使在操作的过程中出现了错误,也可以确保该文件最终会被关闭。

  在执行try...finally语句时,会先执行try子句中的语句序列1。如果语句序列1执行完毕并且没有引发异常,那么finally子句中的语句序列2就会被执行。如果在执行语句序列1的过程中引发了一个异常,那么控制权就会被转移到语句序列2上,一旦语句序列2执行完毕,该异常就会被重新引发。即使通过调用exit、break或continue标准过程来使控制权离开语句序列1,语句序列2仍然会被自动执行。因此,不管try子句是如何结束的,finally子句总是会被执行。

  如果在finally子句中引发了一个异常却没有进行处理,那么该异常将从try...finally语句中传播出来,并且先前在try子句中引发的任意异常都将被丢弃。因此,在finally子句中应该对所有局部引发的异常进行处理,以免打扰其他异常的传播。

  评论这张
 
阅读(567)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017