DELPHI基础教程
第十二章 异常处理与程序调试(二) 12.3 异常响应 异常响应为开发者提供了一个按自己的需要进行异常处理的机制。 try … except … end 形成了一个异常响应保护块。与 finally 不同的是:正常情况下 except 后面的语句并不被执行,而当异常发生时程序自动跳到 except ,进入异常响应处理模块。当异常被响应后异常类自动清除。 下面的例子表示了文件打开、删除过程中发生异常时的处理情况: uses Dialogs; var F: Textfile; begin OpenDialog1.Title := 'Delete File'; if OpenDialog1.Execute then begin AssignFile(F, OpenDialog1.FileName); try Reset(F); if MessageDlg('Erase ' +OpenDialog1.FileName + '?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then begin System.CloseFile(F); Erase(F); end; except on EInOutError do MessageDlg('File I/O error.', mtError, [mbOk], 0); on EAccessDenied do MessageDlg('File access denied.', mtError, [mbOk], 0); end; end; end. 保留字 on … do 用于判断异常类型。必须注意的是: except 后面的语句必须包含在某一个 on … do 模块中,而不能单独存在。这又是同 finally 不同的一个地方。 12.3.1 使用异常实例 上面所使用的异常响应方法可总结为如下的形式: on ExceptionType do { 响应某一类的异常} 这种方法唯一使用的信息是异常的类型。一般情况下这已能满足我们的需要。但我们却无法获取异常实例中包含的信息,比如异常消息、错误代码等。假设我们需要对它们进行处理,那么就必须使用异常实例。 为了使用异常实例,需要为特定响应模块提供一个临时变量来保存它: on EInstance : ExceptionType do … 在当前响应模块中我们可以象使用一个普通对象那样来引用它的数据成员。但在当前响应模块之外不被承认。 下面的代码用于获取异常消息并按自己的方式显示它: {窗口中包括一个 ScrollBar 部件,一个 Button 部件} procedure TErrorForm.Button1Click(Sender: TObject); begin try ScrollBar1.Max := ScrollBar1.Min-1; except on E: EInvalidOperation do MessageDlg('Ignoring Exception:'+E.Message, mtInformation,[mbOK],0); end; end; 12.3.2 提供缺省响应 在异常响应模块中,一般我们只对希望响应的特定异常进行处理。如果一个异常发生而响应模块并没有包含对它的处理代码,则退出当前响应模块,异常类仍被保留。 为了保证任何异常发生后都能在当前响应模块中被清除,可以定义缺省响应: try { 程序正常功能} except on ESomething do { 响应特定异常} else { 提供缺省响应} end; 由于 else 可以响应任何异常,包括我们一无所知的异常,因此在缺省响应中最好只包括诸如显示一个消息框之类的处理,而不要改变程序的运行状态或数据。 12.3.3 响应一族异常 诸如 on ExceptionType do 的异常响应语句不仅可响应本类异常,而且可以响应子类异常。对于象 EIntError 、 EMathError 等系统不会引发的异常,它们将只响应其子类异常。而对于象 on Exception do 这样的语句将会对任何异常进行响应。 下面一段代码对整数越界异常进行单独处理,而对其它整数异常进行统一处理: try { 整数运算} except on ERangeError do { 越界处理} on EIntError do { 其它整数异常处理} end; 由于异常在处理后即被清除,因而上面的代码可保证不会使 ERangeError 异常被多次处理。假如颠倒两条响应语句的顺序,则 ERangeError 异常响应将永远没有被执行的机会。 由于异常在处理后即被清除,因而当希望对异常进行多次处理时就需要使用保留字 raise 来重引发一个当前异常。 下面的代码同时使用了异常响应和异常保护。异常响应用于设置变量的值,异常保护用于释放资源。当异常响应结束时利用 raise 重引发一个当前异常。 var APointer: Pointer ; AInt , ADiv: Integer; begin ADiv := 0; GetMem ( APointer , 1024 ); try try AInt := 10 div ADiv ; except on EDivByZero do begin AInt := 0 ; raise; end; end; finally FreeMem ( APointer , 1024 ); end; end; 上面一段代码体现了异常处理的嵌套。异常保护、异常响应可以单独嵌套也可以如上例所示的那样相互嵌套。 12.3.5 自定义异常类的应用 利用 Delphi 的异常类机制我们可以定义自己的异常类来处理程序执行中的异常情况。同标准异常不同的是:这种异常情况并不是相对于系统的正常运行,而是应用程序的预设定状态。比如输入一个非法的口令、输入数据值超出设定范围、计算结果偏离预计值等等。 使用自定义异常需要: 1. 自己定义一个异常对象类; 2. 自己引发一个异常。 12.3.5.1 定义异常对象类 异常是对象,所以定义一类新的异常同定义一个新的对象类型并无太大区别。由于缺省异常处理只处理从 Exception 或 Exception 子类继承的对象,因而自定义异常类应该作为 Exception 或其它标准异常类的子类。这样,假如在一个模块中引发了一个新定义的异常,而这个模块并没有包含对应的异常响应,则缺省异常处理机制将响应该异常,显示一个包含异常类名称和错误信息的消息框。 下面是一个异常类的定义: type EMyException = Class(Exception) ; 12.3.5.2 自引发异常 引发一个异常,调用保留字 raise ,后边跟一个异常类的实例。 假如定义: type EPasswordInvalid = Class(Exception); 则在程序中如下的语句将引发一个 EPasswordInvalid 异常: If Password <> CorrectPassword then raise EPasswordInvalid.Create('Incorrect Password entered'); 异常产生时把 System 库单元中定义的变量 ErrorAddr 的值置为应用程序产生异常处的地址。在你的异常处理过程中可以引用 ErrorAddr 的值。 在自己引发一个异常时,同样可以为 ErrorAddr 分配一个值。 为异常分配一个错误地址需要使用保留字 at ,使用格式如下: raise EInstance at Address_Expession; 12.3.5.3 自定义异常的应用举例 下面我们给出一个利用自定义异常编程的完整实例。 两个标签框 (Label1 、 Label2) 标示对应编辑框的功能。编辑框 PassWord 和 InputEdit 用于输入口令和数字。程序启动时 Label2 、 InputEdit 不可见。当在 PassWord 中输入正确的口令时, Label2 、 InputBox 出现在屏幕上。此时 Label1 、 PassWord 隐藏。 设计时,令 Label2 、 InputEdit 的 Visible 属性为 False 。通过设置 PassWord 的 PassWordChar 可以确定输入口令时回显在屏幕上的字符。 自定义异常 EInvalidPassWord 和 EInvalidInput 分别用于表示输入的口令非法和数字非法。它们都是自定义异常 EInValidation 的子类。而 EInValidation 直接从 Exception 异常类派生。 下面是三个异常类的定义。 type EInValidation = class(Exception) public ErrorCode: Integer; constructor Create(Const Msg: String;ErrorNum: Integer); end; EInvalidPassWord = class(EInValidation) public constructor Create; end; EInvalidInput = class(EInValidation) public constructor Create(ErrorNum: Integer); end; EInValidation 增加了一个公有成员 ErrorCode 来保存错误代码。错误代码的增加提供了很大的编程灵活性。对于异常类,可以根据错误代码提供不同的错误信息;对于使用者可以通过截取错误代码,在 try...except 模块之外来处理异常。 从以上定义可以发现: EInvalidPassWord 和 EInvalidInput 的构造函数参数表中没有表示错误信息的参数。事实上,它们保存在构造函数内部。下面是三个自定义异常类构造函数的实现代码。 constructor EInValidation.Create(Const Msg: String; ErrorNum: Integer); begin inherited Create(Msg); ErrorCode := ErrorNum; end; constructor EInValidPassWord.Create; begin inherited Create('Invalid Password Entered',0); end; constructor EInValidInput.Create(ErrorNum: Integer); var Msg: String; begin case ErrorNum of 1: Msg := 'Can not convert String to Number'; 2: Msg := 'Number is out of Range'; else Msg := 'Input is Invalid'; end; inherited Create(Msg,ErrorNum); end; 对于 EInvalidInput , ErrorCode=1 表示输入的不是纯数字序列,而 ErrorCode=2 表示输入数值越界。 口令检查是用户在 PassWord 中输入口令并按下回车键后开始的。实现代码在 PassWord 的 OnKeyPress 事件处理过程中: procedure TForm1.PassWordKeyPress(Sender: TObject; var Key: Char); const CurrentPassWord = 'Delphi'; begin if Key = #13 then begin try if PassWord.text <> CurrentPassWord then raise EInvalidPassWord.Create; Label2.Visible := True; InputEdit.Visible := True; InputEdit.SetFocus; PassWord.Visible := False; Label1.Visible := False; except on EInvalidPassWord do begin PassWord.text := ''; raise; end; end; Key:=#0; end; end; 同样,在 InputEdit 的 OnKryPress 事件处理过程中实现了输入数字的合法性检查: procedure TForm1.InputEditKeyPress(Sender: TObject; var Key: Char); var Res: Real; Code: Integer; begin if Key = #13 then begin try val(InputEdit.text,Res,Code); if Code <> 0 then raise EInValidInput.create(1); if (Res > 1) or (Res < 0) then raise EInValidInput.create(2); MessageDlg('Correct Input', mtInformation,[mbOk], 0); Key := #0; except on E:EInValidInput do begin InputEdit.text := ''; MessageDlg(E.Message, mtWarning,[mbOk], 0); end; end; end; end; 由于异常响应后即被清除,所以要显示异常信息,需要另外的手段。在以上两段程序中我们采用了两种不同的方法:在口令合法性检查中,利用异常重引发由系统进行缺省响应;在输入数字合法性检查中,通过异常实例来获取异常信息并由自己来显示它。 以上所举的是一个非常简单的例子,但从中已可以发现:使用自定义异常编程,为程序设计带来了很大的灵活性。 12.3.6 利用异常响应编程 利用异常处理机制不仅能使程序更加健壮,而且也提供了一种使程序更加简捷、明了的途径。事实上,使用自定义异常类就是一种利用异常响应编程的方式。这里我们再讨论几个利用标准异常类编程的例子。 比如为了防止零作除数,可以在进行除法运算前使用 if … then … else 语句。但如果有一系列这样的语句则繁琐程度是令人难以忍受的。这时候我们可能倾向于使用 EDivByZero 异常。例如如下一段程序就远比用 if … then … else 实现简捷明了。 function Calcu(x,y,z,a,b,c:Integer):Real; begin try Result := x/a+y/b+z/c ; except on EDivByZero do Result := 0; end; end; 在 (6.2.3) 记录文件的打开与创建中就是利用异常响应来实现文件的打开或创建。 procedure TRecFileForm.OpenButtonClick(Sender: TObject); begin if OpenDialog1.Execute then FileName := OpenDialog1.FileName else exit; AssignFile(MethodFile,Filename); try Reset(MethodFile); FileOpened := True; except on EInOutError do begin try if FileExists(FileName) = False then begin ReWrite(MethodFile); FileOpened := True; end else begin FileOpened := False; MessageDlg(' 文件不能打开 ',mtWarning,[mbOK],0); end; except on EInOutError do begin FileOpened := False; MessageDlg(' 文件不能创建 ',mtWarning,[mbOK],0); end; end; end; end; if FileOpened = False then exit; Count := FileSize(MethodFile); if Count > 0 then ChangeGrid; RecFileForm.Caption := FormCaption+' -- '+FileName; NewButton.Enabled := False; OpenButton.Enabled := False; CloseButton.Enabled := True; end; 总之,利用异常响应编程的中心思想是虽然存在预防异常发生的确定方法,但却对异常的产生并不进行事前预防,而是进行事后处理,并以此来简化程序的逻辑结构。 12.4 程序调试简介 Delphi 提供了一个功能强大的内置调试器 (Integrated Debugger), 因而对程序的调试不用离开集成开发环境 (IDE) 就可以进行。 程序错误基本可以分为两类,即运行时间错和逻辑错。所谓运行时间错是指程序能正常编译但在运行时出错。逻辑错是指程序设计和实现上的错误。程序语句是合法的,并顺利执行了,但执行结果却不是所希望的。 对于这两类错误,调试器都可以帮助你快速定位错误,并通过对程序运行的跟踪和对变量值的监视帮助你寻找错误的真正原因和解决错误的途径。 程序调试的主要内容可以概括为如下的几方面: 1. 调试的准备和开始; 2. 控制程序的执行; 3. 断点的使用; 4. 检查数据的值。 程序调试只有用户实际上机操作才能真正掌握。在这一节中我们主要对调试中的主要问题和一些关键点进行介绍。至于一些很细小的问题相信读者可以在上机实际应用中掌握,因而没有列出。
12.4.1 调试的准备和开始 在程序开发过程中程序编码和调试是一个持续的循环过程,只有在你对程序进行了彻底的测试后才能交付最终用户使用。为了保证调试的彻底性,在调试前应制定一个详细的调试计划。一般说来应该把程序划分为几个相对独立的部分,分别进行调试,以利于错误的迅速定位,确保每一部分程序都按设计的要求运行。 调试计划准备好后就可以开始程序的调试。 开始一个调试过程包括: 1. 编译时产生调试信息; 2. 从 Delphi 里运行你的程序。 在程序调试过程中,程序的执行完全在你的控制之中。你可以在任何位置暂停程序的执行去检查变量和数据结构的值,去显示函数调用序列,去修改程序中变量的值以便观察不同值对程序行为的影响。 12.4.1.1 产生调试信息 要使用内部调试器必须选中 Option| Environment 菜单 References 页的 Integrated Debugging 检查框。缺省情况下该框被选中。 在开始调试前需要使用 Symbols Debug Information( 调试符号信息 ) 编译工程文件。调试符号信息包含了一个符号表,能够使调试器在程序的源代码与编译器产生的机器代码间建立联系。这样在程序执行中可以同时查看对应的源代码。 Delphi 在缺省情况下自动产生调试符号信息。在集成开发环境中的开关选项是 Option|project 菜单 Compiler Options 页的 Debug Information and Local Symbols 检查框。 当产生的调试符号信息供内部调试器使用时,编译器把调试符号表储存在每个相应的 .dcu 文件中。 如果希望在集成环境外使用 Turbo Debugger ,则需要把调试信息储存在最终的 .exe 文件中。为此需要选定 Option|Project 菜单 Linker 页的 Include TDW Debug Info 检查框。 由于储存调试信息大大增加了执行文件的大小,因而调试完成后应重新生成一个不包含调试信息的执行文件。 12.4.1.2 运行程序 通过调试器 ( 包括内置调试器 ) 运行程序,当程序处于等待状态时,调试器可以获得控制,利用调试器的功能来检查当前程序的状态。通过合理布置屏幕显示,使应用程序运行窗口和 Code Editor( 代码编辑器 ) 互不重叠,可以让用户在它们间方便地切换以观察代码执行的效果。 如果希望使用命令行参数来调试程序,则可以通过 Run|Parameters 菜单打开运行参数对话框进行设置。 12.4.2 程序运行的控制 程序运行控制的方法和使用如下表。 表 12.7 程序运行控制的方法和使用途径 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 方法 使用途径 ─────────────────────────────── 运行到光标位置 ● Code Editor 加速菜单的 Run to Cursor 项 (Run to Cursor) ● Run 主菜单的 Run to Cursor 项 ● F4 跟踪 (Trace Into) ● Run 主菜单的 Trace Into 项 ● Trace Into 加速按钮 ● F7 步进 (Step Over) ● Run 主菜单的 Step Over 项 ● Step Over 加速按钮 ● F8 运行到断点 设置断点并按正常方式运行 暂停程序执行 Run 主菜单的 Program Pause 项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 跟踪和步进都是一种单步执行方式。但“步”的含义不同。对跟踪而言它一次执行一条简单程序语句。当碰到包含调试信息的函数或过程调用时则跳入该函数或过程,并执行其第一条可执行语句。对步进而言它一次执行一条当前模块的可执行语句,而不管该语句是否是函数或过程调用。 运行到光标位置和运行到断点都是程序正常运行到某一确定的源代码位置,而后进入调试状态。但相对于运行到光标位置而言,运行到断点更为灵活。因为断点一次可设置多个,同时也可以对断点设置一定的条件。只有满足该条件程序运行才会中止。 |