什么是嵌套异常的正确方式? - 使用Delphi

3
假设我有三个(或更多)过程,其中一些相互调用,如下所示,任何一个都可能失败。如果它们中的任何一个失败,我希望“主”程序立即记录失败并终止程序。在Delphi中,“传回”异常到每个前置过程调用的正确语法是什么?如果有人能帮助我让主程序的Try/except块识别哪一部分失败,那就更好了!以下是三个过程和主程序的示例伪代码。 (我认为我理解原则,与“引发”有关,但需要一些关于实际语法和应该使用哪些代码的帮助)
//////////////////////////////////////
Procedure DoProcA
begin
try
   begin
   {stuff};  //stuff that might fall
   end;
except
 on E : Exception do 
     begin 
     LogError ('error in A'); 
     end  //on E
end;//try

 //////////////////////////////////////

Procedure DoProcB
begin
try
  begin 
  Do ProcC;  //another proc that might fail
  {other stuff}
  end;
except
 on E : Exception do
     begin
     LogError ('error in B');
     end  //on E
end;//try

 //////////////////////////////////////

Procedure DoProcC
begin
try
  begin 
  {Do stuff}  //even more stuf fthat might fail
  end;
except
 on E : Exception do
     begin
     LogError ('error in C');
     end  //on E
end;//try

 //////////////////////////////////////

 //Main programo
 begin 
 try
    DoProcA;
    DoProcB;
    {other stuff}
 except
   {here I want to be able to do something like
    if failure of A, B or C then
       begin      
       LogError ('Failure somewhere in A, B or C');
       application.terminate;
       end;}
 end; //try
 end.
3个回答

10

处理这个问题的最好方法是删除所有这些异常处理程序。使用像madExcept、EurekaLog、JCL Debug等库来记录使其返回顶层异常处理程序的任何异常。

试图为程序中的每个函数添加异常处理程序是不可行的。这并不是异常的正确使用方式。一般规则是,将异常视为不应该被捕获的事物。它们代表异常行为,因此通常情况下,在引发它们的函数中不知道如何处理它们。

因此,停止尝试处理异常。以不要处理它们为指导原则。如果它们全部到达顶层异常处理程序,请在那里处理它们。如果使用上述库之一,您将能够获得丰富的调试信息,以帮助您理解为什么首先会引发异常。


@David,请问您能解释一下您的意思吗?您是指只在主程序中而不是其他地方使用try except块吗?我很重视您和Remy的评论,因为您们都知道自己在说什么,但在这个问题上似乎存在不同的意见。 - user3209752
5
只有当你真正能够处理异常时才应该处理它,不要实现一个无法分辨的捕获所有异常的方法。 - Sir Rufo
@Sir Rufo。我不是很明白。您是建议我使用David的解决方案还是Remy的解决方案?在您看来,哪个使用了catch all? - user3209752
好的,我明白你在说什么关于这个工具,虽然它们很贵(我不想使用JCL的东西),而且我还不清楚如何在异常发生后优雅地终止程序(如果它正在使用命令行参数运行,否则就不重要了)。 - user3209752
2
你的方法还有一个问题,就是你无法在RTL代码引发异常时立即捕获它们,只能在第一次捕获它们时才能捕获。但坦白地说,在每个函数中都放置异常处理程序简直是疯狂的做法。 - David Heffernan
显示剩余4条评论

6

在记录异常信息后,要求每个函数重新引发该异常,例如:

Procedure DoProcA;
begin
  try
    {stuff};  //stuff that might fall
  except
    on E : Exception do 
    begin 
      LogError ('error in A'); 
      raise; // <-- here
    end;
  end;
end;

Procedure DoProcB;
begin
  try
    DoProcC;  //another proc that might fail
    {other stuff}
  except
    on E : Exception do
    begin
      LogError ('error in B');
      raise; // <-- here
    end;
  end;
end;

Procedure DoProcC;
begin
  try
    {Do stuff}  //even more stuff that might fail
  except
    on E : Exception do
    begin
      LogError ('error in C');
      raise; // <-- here
    end;
  end;
end;

begin 
  try
    DoProcA;
    DoProcB;
    {other stuff}
  except
    on E: Exception do
    begin
      LogError ('Failure somewhere in A, B or C');
      //Application.Terminate; // this is not useful unless Application.Run is called first
    end;
  end;
end.

如果您希望主程序能够识别哪个函数失败了,您需要将该信息通过异常链向下传递,例如:

type
  MyException = class(Exception)
  public
    WhichFunc: String;
    constructor CreateWithFunc(const AWhichFunc, AMessage: String);
  end;

constructor MyException.CreateWithFunc(const AWhichFunc, AMessage: String);
begin
  inherited Create(AMessage);
  WhichFunc := AWhichFunc;
end;

Procedure DoProcA;
begin
  try
    {stuff};  //stuff that might fall
  except
    on E : Exception do 
    begin 
      raise MyException.CreateWithFunc('DoProcA', E.Message); // <-- here
    end;
  end;
end;

Procedure DoProcB;
begin
  try
    DoProcC;  //another proc that might fail
    {other stuff}
  except
    on E : MyException do
    begin
      raise; // <-- here
    end;
    on E : Exception do
    begin
      raise MyException.CreateWithFunc('DoProcB', E.Message); // <-- here
    end;
  end;
end;

Procedure DoProcC;
begin
  try
    {Do stuff}  //even more stuff that might fail
  except
    on E : Exception do
    begin
      raise MyException.CreateWithFunc('DoProcC', E.Message); // <-- here
    end;
  end;
end;

begin 
  try
    DoProcA;
    DoProcB;
    {other stuff}
  except
    on E: MyException do
    begin
      LogError ('Failure in ' + E.WhichFunc + ': ' + E.Message);
    end;
    on E: Exception do
    begin
      LogError ('Failure somewhere else: ' + E.Message);
    end;
  end;
end.

或者:

type
  MyException = class(Exception)
  public
    WhichFunc: String;
    constructor CreateWithFunc(const AWhichFunc, AMessage: String);
  end;

constructor MyException.CreateWithFunc(const AWhichFunc, AMessage: String);
begin
  inherited Create(AMessage);
  WhichFunc := AWhichFunc;
end;

Procedure DoProcA;
begin
  try
    {stuff};  //stuff that might fall
  except
    on E : Exception do 
    begin 
      raise MyException.CreateWithFunc('DoProcA', E.Message); // <-- here
    end;
  end;
end;

Procedure DoProcB;
begin
  try
    DoProcC;  //another proc that might fail
    {other stuff}
  except
    on E : Exception do
    begin
      Exception.RaiseOuterException(MyException.CreateWithFunc('DoProcB', E.Message)); // <-- here
    end;
  end;
end;

Procedure DoProcC;
begin
  try
    {Do stuff}  //even more stuff that might fail
  except
    on E : Exception do
    begin
      raise MyException.CreateWithFunc('DoProcC', E.Message); // <-- here
    end;
  end;
end;

var
  Ex: Exception;
begin 
  try
    DoProcA;
    DoProcB;
    {other stuff}
  except
    on E: Exception do
    begin
      Ex := E;
      repeat
        if Ex is MyException then
          LogError ('Failure in ' + MyException(Ex).WhichFunc + ': ' + Ex.Message)
        else
          LogError ('Failure somewhere else: ' + Ex.Message);
        Ex := Ex.InnerException;
      until Ex = nil;
    end;
  end;
end.

应用程序中存在application.terminate,因为包含所有代码的程序可以手动运行,也可以通过传递参数调用预定作业时运行(我会测试是否是这种情况,只有在这种情况下才终止)。目前我遇到了问题,因为如果发生异常,程序可能会挂起,然后调度程序下次尝试运行作业时会失败,因为作业已经在运行。因此,想法是确保如果使用命令行参数运行,则任何异常都会导致程序正确终止,并最好记录失败原因。 - user3209752
Application.Terminate()仅向调用线程的消息队列发送一个WM_QUIT消息。 Application.Run()运行实际的消息循环。如果您不调用Application.Run(),或以其他方式运行自己的消息循环,则Application.Terminate()无效。只需从主begin/end.块退出即可终止进程。异常本身并不会导致挂起。您的代码必须对捕获的异常做出响应才能挂起,例如显示用户必须(但无法)解除的模态弹出消息框。 - Remy Lebeau
谢谢Remy,非常有用的东西-以及一些指向我的程序可能失败的地方的指针。我确实有几条消息要向交互式用户显示异常。我敢打赌其中一个是在作为计划任务运行时显示的。很难知道应该把答案授予谁,你还是David,因为你们两个总是非常有帮助。在这种情况下,正如David自己所说,他给了我非常有用的建议,但是你直接回答了问题,所以我想获胜者必须是Remy,因为他实际上回答了问题。谢谢你们俩。 - user3209752

3

其他语言的开发者经常花费很多时间和精力担心"未处理的异常",但在典型的Delphi窗体应用程序中,如果你查看Application.Run后面的代码,你会发现如果你让它们一路冒泡到顶部,所有异常都将得到处理。(这是首选的行为,除非你有很好的理由干涉)

通常情况下,添加更多异常信息并重新引发它是一个好主意,也就是说,让它继续传递。出了点问题,你的调用函数需要知道,并且这就是异常的目的。

如果你想记录每一个错误,那么一个好的地方就是在Application.OnException事件中进行。注意,你的例子是一个DOS风格的命令行应用程序,而不是典型的Delphi Windows窗体应用程序,我不确定这是否是你想要的。如果这只是为了尝试保持例子简单,那么你实际上给自己造成了更多的工作,因为你无法访问Application对象和所有相关的功能。

例如:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnException := AppException;
end;

procedure TForm1.AppException(Sender: TObject; E: Exception);
begin
  if RunningInAutomationMode then
    begin
      LogError(E.Message);
      Application.Terminate;
    end
  else
    Application.ShowException(E);

end; 

直接回答您的问题:

Procedure DoProcA;
begin
  try
    {stuff};  //stuff that might fall
  except
    on E : Exception do 
    begin 
      //LogError ('error in A');  will get logged later, don't want to log twice
      E.Message := 'The following error occurred while trying to do whatzit to a whozit: '+E.Message;
      raise; 
    end;
  end;
end;

Procedure DoProcB;
begin
  try
    DoProcC;  //another proc that might fail
    {other stuff}
  except
    on E : Exception do
    begin
      //LogError ('error in B');
      E.Message := E.Message + ' (Proc B)';
      raise; 
    end;
  end;
end;

Procedure DoProcC;
begin
  try
    {Do stuff}  //even more stuff that might fail
  except
     on E : Exception do
    begin
      //LogError ('error in C');
      E.Message := 'The following error occurred during procedure C: '+E.Message;
      raise; //Note: do not use raise Exception.Create(E.Message); as you will then lose the exception's type, which can be very useful information to have
    end;
  end;
end;

begin
   try
     DoProcA;
     DoProcB;
     {other stuff}
  except
    on E: Exception do
    begin
      LogError (E.Message); //this will end up logging all the exceptions, no mater which procedure they occurred in
      //Exception has not been re-raised, so code will continue from this point
      Exit;
    end;
  end;

{Some more code} //Called exit above, so that this code won't get called, although it is unlikely you'll have more code outside the try..except block

end.

是的,这段代码在论坛上进行了简化。实际上它是在主窗体的OnShow事件中。我有一个菜单选项,可以从另一个外部数据库重新填充一个数据库,从在线服务器下载东西,做很多数学运算并将摘要保存在我的数据库中。任何错误消息都会显示给用户。然而,该程序也可以通过调度程序使用命令行参数在夜间完成所有这些工作。我在OnShow中尽早检测到这一点,在这种情况下,我需要这段代码部分具备防弹功能,记录任何错误,以便我知道哪里出错,并始终正常关闭。 - user3209752
好的,那么你只需要在Application.OnException事件中记录错误,就可以了,不需要在其他地方记录。我已经在我的原始答案中添加了一个使用Application.OnException的示例。 - Maya
示例代码有点不清晰 - 有时它使用 E.Message := E.Message +,有时则是 E.Message := 'some value',这取决于调用树,可能会覆盖现有信息。我想最好使用字符串列表或在下一个更高级别的异常中添加“内部”异常的完整数据。 - mjn
嗨mjn,在所有三个过程中,DoProcA、DoProcB和DoProcC,我都保留了原始消息。我只是把消息放在DoProcB的前面,只是为了展示你有自由度去做你喜欢的事情。在现实生活中,我会选择更加一致,并以类似的方式进行格式化。 - Maya
1
如果您有更多信息要添加到异常中,特别是如果您是第一次在自己的过程中引发异常而不是捕获其他人的异常,那么您应该考虑创建和引发自己的异常类型,但这超出了原始问题的范围。 - Maya

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