beta 的第六篇心得:如何判断程序刚才出现了异常(100分)

B

beta

Unregistered / Unconfirmed
GUEST, unregistred user!
声明:本文乃 熊恒(beta) 原创,如要转载请保持文章完整。
如何判断程序刚才出现了异常 --- by 熊恒(beta)
我们都知道,在 try - finally 结构中,无论 try 中的代码是否出现异常,finally
中的代码都一定会执行,那么我们在 finally 部分中能否判断在刚才执行 try 中代码
的时候是否出现了异常呢?答案是肯定的。我这里有两个办法,一个是最容易想到的笨
办法,另一个是相对比较有技巧性的办法。老规矩,重点介绍的放在后面。这两个方法
(实现)都有一个共同的假设:假设你需要一个do
Log 过程来写日志,当然,你需要
在这个过程中判断在该过程调用前是否发生了异常,以便决定日志的内容。
方法一:
proceduredo
Log(const AnyException: Boolean);
begin
if not AnyException then
begin
// 没有发生异常
// ...
end else
begin
// 发生了异常
// ...
end;
end;

procedure Work;
var
AnyException: Boolean;
begin
AnyException := False;
try
// call some func
AnyException := True;
finally
do
Log(AnyException);
end;
end;

可能有人要说了,“晕,这是什么解决办法”,别急,这为什么不是解决办法?它能
解决这个问题,那它就是这个问题的一个解决办法。我觉得,通常情况下,不必把问题
复杂化,很多看似复杂的问题其实反而都有一个简单或者说是“笨”的解决办法,当你
没有其他更好的办法或寻找其他办法所需要的代价相对较大的时候,“笨”一下何妨?
OK,要是真的只有这点东西,我也不好意思专门写一篇心得贴出来,看另一个办法吧。
方法二:
受了 Another_eYes 大虾的启发,本想从堆栈里面找出蛛丝马迹,结果转来转去,蛛
丝和马迹都找到不少,想想应该可以通过一系列复杂的操作达到目的,结果发现这样搞
太复杂,差点害我掉进了沼泽。
就在快要失望放弃的时候,我又有了意外的发现(我怎么总是有意外的发现?运气真
好,呵呵)。在 try 中的最后一行代码执行完过后,finally 中第一行代码执行之前,
编译器会自动插入一段代码,类似这个样子:
xor eax, eax // 清空 eax 寄存器
pop edx // 恢复 edx 的值
pop ecx
pop ecx
mov fs:[eax], edx // 不必管它
push $00458d32 // try - finally 出口
如果在 try 中出现了异常,那么程序会从出错的地点直接跳到内部的出错处理部分,
然后再跳到我们的 finally 部分(严格来说,过程不是这样,不过你可以认为是这样,
对于处理本问题是适用的,要是有朋友想打破沙锅问到底,我再罗嗦也不迟),也就是
说,如果出现了异常,则这一段代码不会被执行到。于是,我们就可以充分利用这段代
码的特点,来实现我们的目的。
其实问题的关键就在这第一行代码,清空 eax,而其后的几行代码都没有修改 eax 寄
存器的值;而刚才我们也说到了,这段代码是插在 finally 中我们自己代码的第一行前
面的。这就是说,我们完全可以在 finally 的第一行判断 eax 的值,就可以知道刚才
是否发生异常了。
我知道,你又会问了,要是出现了异常,但是在跳转到内部出错处理部分过后,在执
行我们的 finally 部分中第一行语句之前,eax 恰好也被修改为了 0 怎么办?据我观
察,在执行我们的 finally 中第一行之前,eax 的值应该是指向堆栈中的某个位置,
而且就在栈顶附近,这是由之前的最后一次函数调用决定的,所以几乎可以说它在这种
情况下肯定不会是 0 的(要是你发现了特例,记得告诉我)。
好了,知道怎么做了,我们先看解决代码吧,其实虽然这个解决方法看起来比较复杂,
其实其解决代码比前一个方法更简单(要不然不是白搞半天,呵呵),还有一个小技巧
在里面。
proceduredo
Log(const Flag: Integer);
begin
if Flag = 0 then
begin
// 没有发生异常
// ...
end else
begin
// 发生了异常
// ...
end;
end;

procedure Work;
begin
try
// call some func
finally
asm calldo
Log end;
end;
end;

刚才不是说了要在 finally 中的第一行判断 eax 吗?怎么没有了?其实是有的,要
知道,在 Delphi 中,普通的过程调用的第一个参数就是放在 eax 中的,所以其实我
是把这个判断放到do
Log 过程中了。
那你说我在 Work 过程中为何要用汇编来调用do
Log 呢?其实,如果不这样,我就
必须显示地去传递一个参数给它,否则编译器不会放过我,但我上哪里找参数传给它?
DoLog(eax)?呵呵,这可不行。
那何不干脆让do
Log 没有参数,直接在do
Log 里面判断 eax 不也可以吗?还是不
行,DoLog 在执行第一行用户代码前,还有动作,而如果你的过程没有参数的话,这些
动作很有可能修改 eax。
哦,那还可以先将 eax 入栈,然后调用无参数的do
Log,然后在do
Log 中将刚才的
eax 弹出来,再判断。晕,这当然可以,不过这不是自找麻烦嘛。
想来想去,还是上面的这几行代码最精练。好了,我也想累了,剩下的交给其他人想吧。
附:我的调试代码
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;
implementation
{$R *.dfm}
proceduredo
Log(const Flag: Integer);
begin
if Flag = 0 then
ShowMessage('No Except')
else
ShowMessage('Any Except');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
i, j: Integer;
x:do
uble;
begin
i := 5;
j := StrToIntDef(Edit1.Text, 0);
try
x := i / j;
// 下面这行可不能少哦,不然你计算出来的 x 没有被用到,
// 就不会计算,就不会引发“被 0 除”异常了
Caption := FloatToStr(x);
finally
asm calldo
Log end;
end;
end;

 
不对啊,不管代码有没有错, 每次都执行 ShowMessage('Any Except');
 
不会吧,我这里怎么一切正常?
我 Win2k pro + D6 的系统。
 
Beta大虾:
是不是应该把Try finally块变成Try Except块啊,因为Finally总是执行啊。
 
有意思!
 
to zqw0117: 正是因为 finally 总是执行,才有必要在里面判断是否出现异常啊,
要是换成 except 那还有必要判断吗?执行了就肯定是有异常啰:)
to All: 有没有问题啊,帮忙试一试啊,我这里没有问题,但怎么有人测试有问题呢?
 
兄弟,瞧不起我,我给分请人回答,你竟然给出答案还送分.......
嘿嘿,米粉可以请,你现在在学校???
DoLog 参数看起来及其不爽,加把油搞定他......
 
不错,有意思,有分得没有?
 
//兄弟,瞧不起我,我给分请人回答,你竟然给出答案还送分.......
呵呵,从你那里得了分,这里就有分发了嘛:)
//嘿嘿,米粉可以请,你现在在学校???
是啊,兄弟们都带着mm出去游漓江了,我一个人在家:(
//DoLog 参数看起来及其不爽,加把油搞定他......
没有参数怎么判断?除非你想麻烦点,DoLog 无参数版本:
proceduredo
Log;
var
Flag: Integer;
// 还不是得用个临时变量,你不会想用汇编控制跳转吧:)
begin
asm
mov eax, [ebp + $08] // 从栈中取出刚才存进去的 eax 的值
mov Flag, eax
end;

if Flag = 0 then
ShowMessage('No Except')
else
ShowMessage('Any Except');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
i, j: Integer;
x:do
uble;
begin
i := 5;
j := StrToIntDef(Edit1.Text, 1);
try
x := i / j;
Caption := FloatToStr(x);
finally
asm
push eax // 将 eax 进栈
calldo
Log // 调用
pop eax // 记得出栈,否则出错别怪我:)
end;
end;
end;
 
哦,好文
 
not cool , 如果do
Log 不在 Finally 里面,不正常。
 
当然啦,情况不一样嘛,所以我当成才设计了有参数的do
Log 就是考虑了这一点的。
在 except 里面就用do
Log(1);
来调用,多方便,何必太在意这个参数呢:)
 
我的做事原则就是追求完美。
 
那你就再加个壳子,DoLogExcept,以备在 except 中调用:)
 
to bata:
要判断try finally 有无异常,不用这么麻烦吧?看下面的:
procedure TForm1.Button1Click(Sender: TObject);
var i:integer;
begin
try
strtoint('a');
......
i:=1;
finally
if i=1 then
showmessage('没异常') else
showmessage('游异常');
end;
end;
to delp:
我记得你的帖子文的是如何在别的函数中判断有无异常,如果要在finally中调用的话,
好像是不符合要求吧?
 
to wfzha: 你写的这个不就是我写的方法一嘛:)
 
我一般采用类似com方法.
创建一个MyExceptionList, 然后写如下代码.
try
//do sth
except
on e:exceptiondo
HandleMyException(e, isAbortable := true|false);
end;
procedure HandleMyException(e, isAbortable default := true);
begin
if e is EAbort then
abort;
// break the execution
AddInMyExceptionList(e);
..
if isAbortable then

abort;
end;
然后写一个GetMyLastError的过程去校验没有abort的过程.
funation GetMyLastError;MyException;
begin
result := MyExceptionList[MyExceptionList.count-1];
NyExceptionList.clear;
end;

另外如果函数定义了Return Code的话, 还可以写一个类似OleCheck过程.
procedure MyCheck(AResult:TMyReturnCode);
begin
if not succeeded(AResult) then
MyError(AResult);
end;

MyCheck(DoSomething);

 
一点疑问,异常对象在 end 过后不就释放了吗,你保存了其指针,那么
在退出异常模块过后,这些指针不就没有用了?
 
没说清楚
MyExceptionList里保存的不是exception 对象, 而是自定义的exception 记录.
type
Myexcetion = record
ErrorCode : integer;
Message : string;
..
end;
AddInMyExceptionList会创建MyException记录,再加进去.
 
顶部