设计健壮程序的关键之一是,如果程序分配了资源,最后就必须释放它,而不管异常是否发生。
例如,如果你的程序分配了一块内存,必须确保最终释放这块内存;如果打开了一个文件,必须确保最后关闭这个文件。
在正常情况下,程序在申请资源后,能够释放它。然而,就算异常发生了,也必须保证释放它。
异常往往由一些偶然的事件导致。例如,程序中调用一个RTL例程,或使用一个构件都可能产生异常。你的程序必须确保即使这些偶然事件发生了,也要释放被占用的资源。
常见的需要保护的资源有:文件、内存、Windows资源 、各种对象等。
try...except...end; try...finally...end; 捕获异常、资源分配! 未知、意外的异常处理,信息提示(异常分支流程,异常抛出)?何时。。如何?精度(粗?细)
但需要强调的是,异常用来标志错误发生,却并不因为错误发生而产生异常。产生异常仅仅是因为遇到了 raise,
在任何时候,即使没有错误发生,raise 都将会导致异常的发生。 注意:异常的发生,仅仅是因为 raise,而非其他!
一旦抛出异常,函数的代码就从异常抛出处立刻返回,从而保护其下面的敏感代码不会得到执行。对于抛出异常的函数本身来说,通过异常从函数返回和正常从函数返回(执行到函数末尾或遇到了 Exit)是没有什么区别的,函数代码同样会从堆栈弹出,局部简单对象(数组、记录等)会自动被清理、回收。
采用抛出异常以处理意外情况,则可以保证程序主流程中的所有代码可用,而不必加入繁杂的判断语句。
“在似乎有用的时候,就应该使用 try...except 块。但是要试着让自己对这种技术的热情不要太过分”。
异常提供了一种更加灵活和开放的方式,使得后来的编程者可以来根据实际的情况处理这种错误,而不是使用预先设定好的处理结果。
异常:理解为一种特殊的事件,该事件发生时,程序的正常执行将被打断.由程序导致的不正常情况是错误而不是异常,程序错误与异常不是相同的概念.
异常是为方便用户报告错误并处理错误而创建的机制,一般是由操作系统完成的运行期错误处理
异常处理的基本思想是通过提供规范方式处理软,硬件错误的能力,使程序更加健壮; 异常处理可以将处理错误的代码与正常的逻辑处理代码相分离.
Delphi缺省的方式是在应用程序收到异常之前捕获异常.IDE会给出一个”预警”对话框,以指明应用程序将要产生异常.
异常处理机制是一种程序设计安全策略,它是建立在保护块思想上,通过try和end语句块对代码的封装确保在程序发生异常时,
9.2 异常处理语句
要处理异常,首先必须识别异常,判断异常在什么时候发生,然后对出现的异常作出处理。
因此对于程序员来说,其工作就是确定程序中哪个地方产生错误及发生错误时的处理程序,特别是容易导致数据或系统资源丢失的错误。
被保护代码块是用户处理异常的基本工具。对异常的响应是在代码保护块中进行的。当有若干条语句需要处理同一个错误时,可以将这些语句放在一个代码块里,并对整个代码块定义一个响应过程。这个对异常响应的代码块称为保护块。
处理代码保护块异常及其异常处理有两种方式,即try...finally语句和try...except语句。这两种语句可以互相嵌套,形成更加牢固的异常处理结构。
9.4 Exception类
Exception类是所有异常类的基类,其本身直接继承于TObject类,在Delphi的Sysutils单元中,Exception类的定义如下(只列出公共成员):
Type Exception=Class(TObject)
public
Constructor Create(const Msg:string);
Constructor CreateFmt(const Msg:string;const Args:array of const);
Constructor CreateRes(Ident:Integer);
Constructor CreateResFmt(Ident:Integer;const Args:array of const);
Constructor CreateHelp(const Msg:string;HelpContext:Integer);
Constructor CreateFmtHelp(const Msg:string;const Args:array of const;HelpContext:Integer);
Constructor CreateResHelp(Ident,HelpContext:Integer);
Constructor CreateResFmtHelp(Ident:Integer;const Args:array of const;HelpContext:Integer);
Property HelpContext:Integer;
Property Message:String;
end;
Exception类中只申明了两个特性:一个是Message特性,用于给出有关异常的简短说明;另一个是HelpContext特性,用于指定帮助的上下文编号,任何一个异常都可以访问这两个特性 。
Exception类提供了一些构造异常的方法。所有构造函数的名字都以Create开头,它们都有相同的目的,就是根据不同的参数来创建一个错误提示信息字符串。用户可以调用这些构造函数来创建自己的派生类的异常实例,或者可以为Exception对象调用它们。
例如,把一个字符串或变量作为错误消息来传递,生成异常的语句可以是:
raise Exception.Create('Error In Memory');
又例如,下面语句生成的异常显示带有两个整数的错误信息字符串:
raise Exception.CreateFmt('Error:X=%d Y=%d',{X,Y]);
下面是这个语句的一个运行结果: Error:X=-1 Y=12
9.5 自定义异常
当用户需要产生异常时,可以选择从Exception中派生出来的一个类或自己创建异常类。用户可以从Exception中派生自己的类,也可以设计一个全新的类,这样的类不需要建立在Exception的基础上。但是,Delphi建议定义异常时,最好用Exception类作为基类,因为不是从Exception中派生的类必须处理所有不是基于Exception的异常,任何未处理的异常都会引起致命的应用程序错误。
自定义异常类的程序如下:
type EMyException = class(Exception)
上面,定义了一个异常类,该类的基类是Exception.
当然,异常不一定必须直接从Exception类继承,也可以用其它Exception类的派生类作为自己的基类,例如:
type
EMathError = class(Exception);
EInvalidOp = class(EMathError);
EZeroDivide = class(EMathError);
EOverflow = class(EMathError);
EUnderflow = class(EMathError);
在声明异常时,可以根据需要声明一些字段、方法和特性。例如,SysUtils单元中的EInOutError异常就声明了一个ErrorCode字段,用于存储引起异常的文件I/O错误代码,示例如下:
type EInOutError = class(Exception)
ErrorCode: Integer;
end;
声明了自己的异常后,需要在程序中触发这个异常,使用Raise语句。Raise语句的语法格式:
raise object at address
这里object和 at address都是可选的。如果省略了object,表示要重新触发当前异常。 这种形式只能出现在try...Except结构的Except部分,主要用于过程或函数的调用中出现无法完全处理异常情况。raise后面的object是异常的对象实例,而不是异常的类型。通常把创建对象实例与触发这个异常并在一句中,因为构造函数返回的总是该类型的对象实例。如
EPasswordInvalid = class(Exception);
在触发异常时,创建对象实例与触发这个异常并在一句中。
if Password <> CorrectPassword then
raise EPasswordInvalid.Create('Incorrect password entered');
注意:虽然程序创建了异常对象实例,但程序不必自己删除它,因为异常处理句柄会自动删除异常的实例。
关键步骤是程序中定义了一个TMouseException类。其定义方法是:
type
TMouseException = class(Exception)
X, Y: Integer;
constructor Create(const Msg: string; XX, YY: Integer);
end;
TMouseException类是从Exception类派生出来的。除了从Exception继承来的成员外,TMouseException类还增加了整型成员X和Y,并说明了一个构件函数,可以调用这个构造函数来初始化该类的对象。其构造函数的编程为:
constructor TMouseException.Create(const Msg: string; XX, YY: Integer);
begin
X := XX; // Save X and Y values in object
Y := YY;
Message := // Create message string Msg + ' (X=' + IntToStr(X) + ', Y=' + IntToStr(Y) + ')';
end;
这里,Message是从Exception来继承过来的,它被赋值给了包括这两个值的一个消息。
Width := 65;{ Change inherited properties }
Height := 65;
FPen := TPen.Create;{ Initialize new fields }
FPen.OnChange := PenChanged;
FBrush := TBrush.Create;
FBrush.OnChange := BrushChanged;
end;
构造的第一行是Inherited Create(Owner),其中Inherited是保留字,Create是祖先类的构造名,事实上大多数构造都是这么写的。这句话的意思是首先调用祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派生类的字段,当然也可以重新对祖先类的字段赋值。用类来引用构造时,程序将自动做一些缺省的初始化工作,也就是说,对象在被创建时,其字段已经有了缺省的值。所有的字段都被缺省置为0(对于有序类型)、nil(指针或类类型)、空(字符串)、或者 Unassigned (变体类型)。除非想在创建对象时赋给这些字段其它值,否则在构造中除了Inherited Create(Owner)这句外,不需要写任何代码。
如果在用类来引用构造的过程中发生了异常,程序将自动调用析构来删除还没有完全创建好的对象实例。效果类似在构造中嵌入了一个try協inally语句,例如:
try
...{ User defined actions }
except{ On any exception }
Destroy;{ Destroy unfinished object }
raise;{ Re-raise exception }
end;
构造也可以声明为虚拟的,当构造由类来引用时,虚拟的构造跟静态的构造没有什么区别。当构造由对象实例来引用时,构造就具有多态性。可以使用不同的构造来初始化对象实例。
2.析构
析构的作用跟构造正相反,它用于删除对象并指定删除对象时的动作,通常是释放对象所占用的堆和先前占用的其它资源。构造的定义中,第一句通常是调用祖先类的构造,而析构正相反,通常是最后一句调用祖先类的析构,程序示例如下:
destructor TShape.Destroy;
begin
FBrush.Free;
FPen.Free;
inherited Destroy;
end;
上例中,析构首先释放了刷子和笔的句然后调用祖先类的析构。
析构可以被声明为虚拟的,这样派生类就可以重载它的定义,甚至由多个析构的版本存在。事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是一个虚拟的无参数的析构,这样,所有的类都可以重载Destroy。
前面提到,当用类来引用构造时,如果发生运行期异常,程序将自动调用析构来删除还没有完全创建好的对象。由于构造将执行缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段释放为Nil。有一个比较稳妥的办法是,用Free来释放占用的资源而不是调用Destroy,例如上例中的FBrush.Free和FPen.Free,Free方法的实现是:
procedure TObject.Free;
begin
if Self <> nil then Destroy;
end;
也即Free方法在调用Destroy前会自动判断指针是否为Nil。如果改用FBrush.Destroy和FPen.Destroy,当这些指针为Nil时将产生异常导致程序中止。