在记录Delphi异常后如何重新引发该异常?

27

你知道在Delphi代码中捕获、记录并重新抛出异常的方法吗?以下是一个简单的示例:

procedure TForm3.Button1Click(Sender: TObject);
begin
  try
    raise Exception.Create('Bum');
  except
    on E: Exception do
    begin
      MyHandleException(E);
    end;
  end;
end;

procedure TForm3.MyHandleException(AException: Exception);
begin
  ShowMessage(AException.Message);
  LogThis(AException.Message);  
  // raise AException; - this will access violate
end;

所以我需要在except块中重新抛出它,但我想知道是否有更好的方法来编写自己的方法来处理并(在特定条件下)重新抛出异常。


4
用通常的方法重新引发它。这样可以使你的代码自文档化,并且不会混乱堆栈跟踪。 - Igby Largeman
是的,我绝对想要保留堆栈! - Nik Todorov
3
如果你想记录异常,你应该看看madExcept、EurekaLog和/或Jedi。它们都有比你自己实现的更好的异常记录处理方式。 - Lieven Keersmaekers
1
是的,我知道这些工具,我更喜欢EurekaLog,但我不能决定是否使用它以及如何与客户一起使用这些工具...所以我正在尽我所能 :) - Nik Todorov
2
异常是引用计数的。当EXCEPT块退出时,触发异常会递减。RAISE自身没有异常实例会增加引用计数。 RAISE后跟异常实例(例如raise E;)将不会增加引用计数,因此会出现AV提示。Exception.Create()将导致引用计数为1的异常,并且如果在异常块中执行,则不会在EXCEPT块退出时受到引用计数递减的影响。编写代码,其中可以调用仅使用raise或创建新异常或使用AcquireExceptionObject()来增加引用计数,如下面的解决方案所述。 - FreeText
8个回答

37

如果你想在特定条件下重新引发异常,可以编写以下代码:

procedure TForm3.Button1Click(Sender: TObject);
begin
  try
    raise Exception.Create('Bum');
  except
    on E: Exception do
    begin
      if MyHandleException(E) then
        raise;
    end;
  end;
end;

function TForm3.MyHandleException(AException: Exception): boolean;
begin
  ShowMessage(AException.Message);
  result := true/false;
end;

1
这是个好主意,我也有这个想法,但我还想知道是否有其他无需编写代码的方法来在单独的函数中实现它。谢谢。 - Nik Todorov
虽然这是解决这个问题最正确的方法,但有些情况下你无法修改调用方法(也许是别人的代码,例如组件的事件处理程序——我正好遇到了这种情况),所以对我来说,Craig和特别是Wes的解决方案才是这个问题的真正答案。 - Bozzy

12

接着Craig Young的帖子,我成功地使用了以下类似的代码。通过使用ExceptAddr函数的“at”标识符,您可以保留原始异常位置。原始异常类类型和信息也得以保留。

procedure MyHandleException(AMethod: string);
var
  e: Exception;
begin
  e := Exception(AcquireExceptionObject);
  e.Message := e.Message + ' raised in ' + AMethod; 
  raise e at ExceptAddr;
end;

try
  ...
except
  MyHandleException('MyMethod');
end;

8
以下代码可以工作,但显然不是最理想的,原因有两点:
- 异常被从调用栈的不同位置抛出。 - 您无法获得异常的精确副本 - 特别是那些添加属性的类。也就是说,您必须显式地复制所需的属性。 - 复制自定义属性可能会变得混乱,因为需要进行类型检查。
procedure TForm3.MyHandleException(AException: Exception);
begin
  ShowMessage(AException.Message);
  LogThis(AException.Message);  
  raise ExceptClass(AException.ClassType).Create(AException.Message);
end;

优点是您可以保留原始异常类和消息(以及任何其他属性,您希望复制的属性)。

理想情况下,您希望调用System._RaiseAgain,但遗憾的是,这是一种“编译器魔法”程序,只能由raise;调用。


+1:我一直在寻找一种方法,在try...except块之外重新引发异常。我知道你会失去调用堆栈,但是对于在另一个线程中发生的主VCL线程中重新引发异常,这非常有效。谢谢! - James L.

4

您可以尝试使用(system.pas):

function AcquireExceptionObject: Pointer;

AcquireExceptionObject返回指向当前异常对象的指针并防止在当前异常处理程序退出时释放异常对象。
注意:AcquireExceptionObject会增加异常对象的引用计数。确保在不再需要异常对象时将引用计数递减。如果使用异常对象重新引发异常,则会自动发生这种情况。在所有其他情况下,每次调用AcquireExceptionObject都必须有一个匹配的ReleaseExceptionObject调用。AcquireExceptionObject/ReleaseExceptionObject序列可以嵌套。

有趣,我会尝试一下。谢谢您提供的详细信息。 - Nik Todorov
注意--至少在Delphi 2007中,ReleaseExceptionObject是一个空方法。与AcquireExceptionObject一起使用会导致内存泄漏。我已经发现FreeAndNil可以作为替代方法。 - Chad N B
在D2009中,“ReleaseExceptionObject”也是空的。我想知道文档是否正确关于这个引用计数。异常是从TObject而不是TInterfacedObject派生的。 - Wodzu

2

您应该可以直接使用Raise命令来重新引发异常:

begin
  MyHandleException(E);
  Raise;
end;

1
我认为这正是他试图避免的:每次调用MyHandleException时都必须在其后面放置raise,而是将raise放在MyHandleException内部。 - Mason Wheeler
@Mason:你可能是正确的。我可能读错了。我会稍后删除我的回答,这样你就有机会看到这个评论 :) - Mark Wilkins
我认为这是正确的。他把raise放在了一个在except块内调用的过程中,这是不起作用的。raise需要精确地放在except块中。如果你在外部过程中写raise AException,你会得到一个访问冲突。 - Andreas Rejbrand
4
不要删除你的答案。你的代码片段可能对那些想知道如何在except块中重新“raise”异常的人有用。并学习到它在其他任何地方都不可能做到。 - PA.
这很有道理 - 我会保留它,因为我认为这些信息本质上并没有错误。 - Mark Wilkins
是的,我正在尝试避免在所有try except块上引发异常。但目前似乎无法避免 :) 谢谢大家! - Nik Todorov

1
老话题了,但是这个解决方案怎么样?
procedure MyHandleException(AException: Exception);
begin
  ShowMessage(AException.Message);
  AcquireExceptionObject;
  raise AException;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  try
    raise Exception.Create('Bum');
  except
    on E: Exception do
      MyHandleException(E);
  end;
end;

这是基于Eduardo发布的第一份代码。


1
这种方法对我很有效!
procedure RaiseExceptionFmt(const AFormat: string;
  const AArgs: array of const);
begin
  raise Exception.CreateFmt(AFormat, AArgs) at ExceptAddr;
end;

我重写了我的方法,现在抛出的异常与之前相同。
procedure RaiseInternalExceptionFmt(const AFormat: string;
  const AArgs: array of const);
var
  LExceptionPtr: Pointer;
begin
  LExceptionPtr := AcquireExceptionObject();
  try
    Exception(LExceptionPtr).Message := Format(AFormat, AArgs);
    raise Exception(LExceptionPtr) at ExceptAddr;
  finally
    ReleaseExceptionObject();
  end;
end;

这个会起作用,但它总是引发一个类型为Exception的异常,即使原始异常是不同的类型,比如EFileNotFound。 - dummzeuch

0

在调用处理程序之前,您可以获取异常对象并将处理程序本身保持为单行。但是,您仍然需要承担大量的“Try / Except / Do / End”负担。

Procedure MyExceptionHandler(AException: Exception);
Begin
  Log(AException); // assuming it accepts an exception
  ShowMessage(AException.Message);
  raise AException; // the ref count will be leveled if you always raise it
End;

Procedure TForm3.Button1Click(Sender: TObject);
Begin
  Try
    Foo;
  Except On E:Exception Do
    MyExceptionHandler(Exception(AcquireExceptionObject));
  End;
End;

然而,如果你只是想要摆脱事件处理程序中重复的错误处理代码,你可以尝试这个方法:

Procedure TForm3.ShowException(AProc : TProc);
Begin
  Try
    AProc;
  Except On E:Exception Do Begin
    Log(E);
    ShowMessage(E.Message);
  End; End;
End;

将您的事件处理程序代码简化为以下内容:

Procedure TForm3.Button1Click(Sender: TObject);
Begin
  ShowException(Procedure Begin // anon method
    Foo; // if this call raises an exception, it will be handled by ShowException's handler
  End);
End;

你也可以让它适用于函数,使用参数化函数:

Function TForm3.ShowException<T>(AFunc : TFunc<T>) : T;
Begin
  Try
    Result := AFunc;
  Except On E:Exception Do Begin
    Log(E);
    ShowMessage(E.Message);
  End; End;
End;

并使ShowException返回一个值(作为传递):

Procedure TForm3.Button1Click(Sender: TObject);
Var
  V : Integer;
Begin
  V := ShowException<Integer>(Function : Integer Begin // anon method
    Result := Foo; // if this call raises an exception, it will be handled by ShowException's handler
  End);
End;

甚至可以让匿名过程直接触及外部作用域变量。
Procedure TForm3.Button1Click(Sender: TObject);
Var
  V : Integer;
Begin
  ShowException(Procedure Begin // anon method
    V := Foo; // if this call raises an exception, it will be handled by ShowException's handler
  End);
End;

在匿名函数内部和外部作用域中定义的变量之间存在一些交互限制,但对于像这样简单的情况,您将会非常好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接