第 3章 异常及错误处理
健壮的程序来自于正确的错误处理。
相信我,总会有意外的……
Delphi 高手突破
正如同现实生活中我们不可能事事如意,你所写的代码也不可能每一行都能得到正确
的执行。生活中遇到不如意的事情,处理好了,雨过天晴;处理不好,情况会越变越糟,
甚至一发而不可收拾,后果难料。程序设计中同样如此,所谓健壮的程序,并非不出错的
程序,而是在出错的情况下能很好地处理的程序。
因此,错误处理一直是程序设计领域的一个重要课题。而异常就是面向对象编程提供
的错误处理解决方案。它是一个非常好的工具,如果你选择了 OOP,选择了 Delphi,那么
异常也就成为你的惟一选择了。
要让你信服地选择异常,需要给出一些理由。在本章中会让你清楚明白地了解异常所
带来的好处。
3.1 异常的本质
什么是异常?为什么要用它?
在基于函数的结构中,一般使用函数返回值来标明函数是否成功执行,并给出错误类
型等信息。于是就会产生如下形式的代码:
nRetVal := SomeFunctionToOpenFile();
if nRetVal = E_SUCCESSED then // 成功打开
begin
……
end
else if nRetVal = E_FILE_NOT_FOUND then // 没有找到文件
begin
……
end
else if nRetVal = E_FILE_FORMAT_ERR then // 文件格式错
begin
……
end
else then
begin
……
end
使用返回错误代码的方法是非常普遍的,但是使用这样的方法存在两个问题:
(1)造成冗长、繁杂的分支结构(大量的 if 或 case 语句),使得程序流程控制变得
复杂,同时造成测试工作的复杂,因为测试需要走遍每个分支。
·50·
异常及错误处理
(2)可能会存在没有被处理的错误(函数调用者如果不判断返回值的话)。
异常可以很好地解决以上两个问题。
所谓“异常”是指一个异常类的对象。Delphi 的 VCL 中,所有异常类都派生于 Exception
类。该类声明了异常的一般行为、性质。最重要的是,它有一个 Message 属性可以报告异
常发生的原因。
抛出一个异常即标志一个错误的发生。使用 raise 保留字来抛出一个异常对象,如:
3
raise Exception.Create(′An error occurred!′);
但需要强调的是,异常用来标志错误发生,却并不因为错误发生而产生异常。产生异
常仅仅是因为遇到了 raise,在任何时候,即使没有错误发生,raise 都将会导致异常的发生。 注意:异常的发生,仅仅是因为 raise,而非其他!
一旦抛出异常,函数的代码就从异常抛出处立刻返回,,从而保护其下面的敏感代码不
会得到执行。对于抛出异常的函数本身来说,通过异常从函数返回和正常从函数返回(执
行到函数末尾或遇到了 Exit)是没有什么区别的,函数代码同样会从堆栈弹出,局部简单
对象(数组、记录等)会自动被清理、回收。
采用抛出异常以处理意外情况,则可以保证程序主流程中的所有代码可用,而不必加
入繁杂的判断语句。
例如,函数 A抛出异常:
function A() : Integer;
vat
pFile : textfile;
begin
…… // 一些代码
pFile := SomeFunctionToOpenAnFile();
if pFile = nil then
raise Exception.Create(′Open file failed!′); // 文件打开失败抛出异常
Read(pFile, ……); // 读文件
…… // 其他一些对文件的操作,此时可以保证文件指针有效
end;
函数 A的代码使得对文件打开的出错处理非常简单。如果打开文件失败,则抛出一个
Exception 类的异常对象,函数立刻返回,从而保护了以下对文件指针的操作不被执行。而
之后的代码可以假设文件指针肯定有效,从而令代码更加美观。
生活中,我们每天扔掉的垃圾都会有清洁工人收拾、处理,否则生活环境中岂不到处
充斥着垃圾?同样,抛出的异常也需要被捕获和处理。假设函数 B 调用了函数 A,要捕获
这个文件打开失败的异常,就需要在调用 A 之前先预设一个陷阱,这个陷阱就是所谓的
“try…except 块”。
·51·
Delphi 高手突破
先看一下函数 B 的代码:
procedure B();
begin
…… // 一些代码
try
A(); // 调用A
SomeFunctionDependOnA(); // 依赖于A的结果的函数
Except
ShowMessage(′some error occured′); // 嘿嘿,掉进来了,发生异常
End;
…… // 继续的代码
end;
A抛出的异常,会被 B所设的 try…except 所捕获。一旦捕获到异常,就不再执行之后
的敏感代码,而是立刻跳至 except 块执行错误处理,处理完成后再继续执行整个 try 块之
后的代码。程序流程的控制权被留在了函数 B。
如果不喜欢自己收拾垃圾,因而在 B 中并没有预设 try…except 块的话,则异常会被继
续抛给 B 的调用者,而如果 B 的调用者同样不负责任,则异常会被继续像踢足球一样被踢
给更上层的调用者,依此类推。不过,不用担心,我们有一个大管家,大家都不要的烫手
山芋,它会帮我们收拾,那就是——VCL(Delphi 的应用程序框架)。
因为 VCL 的框架使得所编写的整个应用程序被包在一个大的 try…except 中,无论什
么没有被处理的异常,最终都会被它所捕获,并将程序流程返回到最外层的消息循环中,
决无遗漏!这也就是为什么会看到很多用 Delphi 所编写的但并不专业的小软件有时会跳出
一个报告错误的对话框(如图 3.1 所示)。发生这样的情况应该责怪软件的编写者没有很
好地处理错误,但有些不明白异常机制的程序员常常会责怪 Delphi 编写的程序怎能会有这
样的情况发生。其实出现这个提示,应该感谢 VCL的异常机制让程序可以继续运行而不是
“非法终止”。
图3.1 异常被VCL所捕获 注意:VCL 用一个大的 try…except 将代码包裹起来!
因此,在 VCL 框架中不会有不被处理的异常,换句话说,也就是不会有不被处理的错
误(虽然笔者说过异常并不等于错误)。对异常的捕获也非常简单,不见了一大堆的 if 或
·52·