异常处理
每一段程序都有可能出错!这是软件业的一个不容置疑的现象和规律。事实上,传统的 if…else…结构完全可以解决所有的错误,使用 Exception 机制也没能够回避在最原始的层次,通过遍历可能的情况来产生异常的做法,那么,为什么还需要异常机制
答案很简单:异常提供了一种更加灵活和开放的方式,使得后来的编程者可以来根据实际的情况处理这种错误,而不是使用预先设定好的处理结果。实际上,这就是异常处理机制的核心
# 使用场景
异常信息的处理方法(写日志、抛出异常),函数或过程出现异常时,只能抛出异常,禁止弹出对话框架。
函数及过程、控件中发生异常,通常是直接抛出异常,不要显示信息提示框;
界面操作(按钮)中的异常,可以显示提示信息(视具体应用而定),也可以不显示提示而将异常信息保存到日志文件或两种方式同时使用;
DLL文件中的异常:如果是函数或过程,发生异常时就直接抛出异常;如果是界面操作,则按界面操作(按钮)中的异常处理方法处理。
# 设计期异常
这种异常类型发生在设计期,通常是因为给组件的某个属性输入了非法的值。例如,在设计数据库应用程序时指定了一个没有定义的数据库别名。
这种类型的异常比较容易被发现和纠正,因为Delphi能够对属性的值进行合法性检查。一旦发现这种错误,Delphi将弹出一个警告窗口,提示你纠正错误。
# 编译期异常
编译期异常也叫语法错误,当程序代码违反了Object Pascal的语法规则时将发生这种错误。换一种更为直白的说法就是代码写错了
如果程序代码中有语法错误,编译就不能通过,代码编辑器的状态栏将给出错误信息提示,并在代码编辑器中突出显示有语法错误的行。
比较常见的语法错误是数据类型不匹配,特别是调用RTL、VCL或Windows的API时容易发生参数不匹配的错误。
# 运行期异常
程序虽然通过了编译,但在运行时失败了,这种错误称为运行期异常
这种异常类型通常情况下不容易被肉眼看出来,例如,程序试图打开一个不存在的文件,在运算时出现了被零除、使用一个空对象调用了该类的成员。
# 相关语法
Delphi中异常的基类是System.SysUtils.Exception,换句话说就是所有的异常类都是Exception的子类。参考Delphi这种异常分类处理方式,我们也可以从Exception派生一个自定义的异常类
有人可能觉得没有自己定义异常类的必要,其实不然,就算Delphi定义的类型够多它也无法囊括所有的异常类型,而针对一些特定的场景则需要我们自己定义异常类
# raise
在讲如何处理异常之前我们需要先了解异常是如何产生的,很简单异常的处理过程就类似一个推卸责任的过程。。。
raise这个关键字的主要作用是抛出一个携带异常信息的对象,而这个对象所对应的类必须是Exception或者其子类,如果在抛的过程中没有遇到处理异常的程序会一直到系统为止,具体的语法如下
raise Exception.Create('我抛出的异常!');
2
3
# try…except…end
在try 体内的代码发生异常时,系统将转向except 部分进行异常的处理。这是Delphi处理异常的最基本的方式之一。
只有当try 体内的代码发生异常时,才会跳转到except 里面的代码进行执行
需要注意的是在excpt...end之间的代码依然有可能存在异常
# try…finally…end
这种异常处理结构一般用于保护windows的资源分配等方面,它确保了无论try 体内的代码是否发生异常,都需要由系统进行最后的统一处理的一些Windows对象的正确处理。和try…except…end不同,该结构的finally部分总被执行
需要注意的是:不存在try…except…finally…end 结构 。其实我个人而言更喜欢这种结构,但是在Delphi中不存在这种结构
# 自定义一个异常
根据前面提到的规则,定义一个异常就是定义一个Exception类的派生类,所以我们可以得到下面的代码
type
TArithmeticException = class(Exception)
public
constructor Create(Msg: string); overload;
end;
constructor TArithmeticException.Create(Msg: string);
begin
inherited Create(Msg);
end;
2
3
4
5
6
7
8
9
10
11
12
13
当然,基类可以是Exception或者是Exception的任何一个任何层次的派生类
其实构造方法可以不进行重写,但是为了代码更为规范和较高的可读性建议还是写上比较好
# 抛出异常
根据不同的情况抛出异常是使用异常的最基本的模式。我的理解可能不是特别准确,所谓的抛出异常其实就是声明一个异常对象
例1
begin
raise TArithmeticException.Create('抛出异常');
end;
2
3
例2
begin
{此处因为本类没有声明这种格式的函数,所以默认引用的是父类的构造函数}
raise TArithmeticException.CreateFmt('%s %d', ['错误代码:', 999]);
end;
2
3
4
例3
var
exc: Exception;
begin
{这种方式和例1没有区别,仅仅是多了一个变量的声明}
exc := TArithmeticException.Create('发现异常');
raise exc;
end;
2
3
4
5
6
7
# 异常类型的精确处理
可以使用 on E:异常类 do... 结构可以在 do 体内处理特定异常类所抛出的异常。
如果在except中使用 on E: 异常类 do… 的话,在except中的 on E: 异常类 do…之外不能有任何语句,例如下面的语法是正确的
try
...
except
on E: Exception do
begin
ShowMessage('OK');
ShowMessage('OK Again');
end;
end;
2
3
4
5
6
7
8
9
但是下面的方式就错了
try
...
except
on E: Exception do
begin
ShowMessage('OK');
end;
//如果在except中使用 on E: 异常类 do 的话,在except中的 on E: 异常类 do之外不能有任何语句
ShowMessage('Not OK');
end;
2
3
4
5
6
7
8
9
10
整个的异常处理代码
procedure Demo1();
begin
raise TArithmeticException.Create('算术异常');
end;
begin
try
try
Demo1();
except
on E: TArithmeticException do begin
{Message即为我们前面创建对象时赋值的内容}
Writeln(E.UnitName,E.ClassName, e.Message);
end;
on E: Exception do begin
Writeln('未知异常');
end;
end;
finally
// 此处作为资源释放的代码
end;
end.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
通过最后的代码可以发现,我们对于代码异常情况的处理更为详细,一旦程序出错更便于我们进行定位。