Delphi 错误处理:Raise vs Exit on try...except...end

Delphi Error handling : Raise vs Exit on try...except...end

在 try except 上调用 Exit 是否安全? 或者我应该打电话给 raise 吗?

我尝试了下面的示例和 raise 示例,跟踪通过了 Delphi 的内部库代码。退出只是退出程序,仅此而已。

我了解到最好保留应用程序堆栈或队列或类似的东西。调用 exit 会破坏堆栈吗?

示例 1(加注)

SDDatabase1.StartTransaction;
Try
  SDQuery1.ApplyUpdates;
  SDDatabase1.Commit;
  SDQuery1.CommitUpdates;
Except
  SDDatabase1.Rollback;
  SDQuery1.RollbackUpdates;
  raise;
End;
..............//other codes I don't want to execute

示例 2(退出)

SDDatabase1.StartTransaction;
Try
  SDQuery1.ApplyUpdates;
  SDDatabase1.Commit;
  SDQuery1.CommitUpdates;
Except
  SDDatabase1.Rollback;
  SDQuery1.RollbackUpdates;
  MessageDlg('Save Failed because: '+E.Message, mtError, [mbOK], 0);
  exit;
end;
..............//other codes I don't want to execute

原则上两者都是安全的,但不可能推荐其中一种方法。这取决于你的设计意图。您必须根据代码的预期用途来决定哪个是合适的。

如果在这段代码中处理异常,把函数留给exit,那么执行returns到调用函数,不知道函数是否成功或失败。这可能有问题。

如果您重新引发异常,执行将移动到调用堆栈上层的下一个合适的异常处理程序,沿途经过任何 finally 块。

因此,行为会有所不同,由您决定您想要哪种。

初学者常犯的错误是尝试在调用堆栈的下方处理异常,而不是理想情况。例如,假设您希望您的代码同时用于 GUI 应用程序和非可视化应用程序。您在非可视化应用中使用 MessageDlg 是不合适的。

在 GUI 应用程序中,大多数操作都是响应用户输入的,例如按下按钮。异常通常会导致整个操作中止。在这种情况下,您根本不应尝试处理异常。让它们传递给应用程序级异常处理程序。

最后,您的代码以相同的方式处理所有异常。这通常是不明智的。例如,访问冲突肯定应该与数据库错误区别对待。

很难客观地评估替代选项(A 与 B),因为其中一个比另一个 "always better"。这就是为什么正确理解每种差异和含义很重要的原因。

当单独检查单个方法时,您的两个示例都跳过 except 块结束后的代码。但是,其中一个处于 异常状态 而另一个则没有。这对您编写的方法没有影响,但对您的方法的 调用者(直接和间接)有影响。

procedur Caller1;
begin
  //...[A]
  Caller2;
  //...[B]
end;

procedure Caller2;
begin
  //...[C]
  CallDatabaseMethod; {Will raise; or Exit; based on example chosen}
  //...[D]
end;

你的两个例子之间的主要区别是:

  • 示例 1 能够向上报告调用堆栈的失败状态。
  • 示例 2 隐藏了此信息,因为异常处理程序 吞噬了 异常。

示例 1 也会跳过 [B] 和 [D] 代码。但是,示例 2 将执行 [B] 和 [D] 代码。当你明白了这个区别后,你就有权决定[B]和[D]是否应该执行。

然而,我怀疑更多时候,CallDatabaseMethod 失败这一事实表明,不应调用 [B] 和 [D]。例如。假设数据库方法更新了客户帐户数据,并且 [B] 和 [D] 执行与发送最新语句相关的操作。 您可能不想在更新失败时发送声明

就是说,如果你的方法可以被认为是 "successfully completed" 尽管有 异常,那么无论如何吞下异常是完全可以接受的。例如。假设您有一个 "Add a Row" 方法,它的 post 条件只是该行必须存在于数据库中。然后,如果您的数据库 return 违反 PK,显然行 确实存在 在这种情况下,吞下异常是完全合理的。


您当然可以调整示例 2 的实现,以免隐藏错误

如果您的方法被编写为 return 成功或失败状态的函数,那么调用者可以使用它来解决上述问题。例如

function Caller1: Boolean;
begin
  Result := Caller2;
  {Caller can decide to skip/ignore/do something different}
  if Result then ...
end;

function Caller2: Boolean;
begin
  Result := CallDatabaseMethod;
  {Caller can decide to skip/ignore/do something different}
  if Result then ...
end;

function CallDatabaseMethod: Boolean;
begin
  Result := True;
  //...
  try
    //...
  except
    on E: ExceptionType do
    begin
      //...
      Result := False;
    end;
  end;
  //...
end;

这与 Windows API 的工作方式相同。它确实有其优点和缺点:

  • 使用return代码意味着调用者必须记得检查错误。 (此站点上 WinAPI 问题的一个常见来源涉及程序员未能通过 API 函数检查错误 return。)
  • 因此,异常模型不能被调用者 "ignored" 显然是一个优势 - 它们最终会浮出水面,即使它涉及崩溃应用程序。
  • 但反过来又存在一个缺点,即忽略强加给您的异常的代码更加混乱。
  • 同样重要的是要注意不要陷入大量代码 运行 除了块 1.
  • 结构异常处理的另一个缺点是它确实具有显着的性能开销,因此理想情况下您不希望过于频繁地引发和处理它们。

我建议最好的方法是确定可以考虑哪些类型的错误 "normal" 并确保使用显式错误结果而不是异常来处理它。当然,上述 1 的实例是主要候选对象。


最后,David 已经在示例 2 中标记了对您的消息对话框的担忧。因此此评论是基于这样的假设:此代码始终 运行 在用户上下文中。

我理解立即显示消息的冲动。在异常传播到应用程序级处理程序时,您的上下文已经丢失。要考虑的一种选择是使用 Abort,它只会引发 EAbort 异常。

try
  //...
except
  on E: ExceptionType do
  begin
    MessageDlg(...);
    Abort;
  end;
end;

默认的应用程序异常应该忽略这个异常并且不显示消息。如果您有自己的处理程序,您应该在显示任何消息之前类似地检查异常 class。


作为旁注,我想考虑问题中的一个特定句子。

I read that it's better to preserve application stack or queue or something like that.

显然,如果您不确定所阅读的内容,就很难向您解释。根据我前面部分的回答,您可能已经有了更清晰的认识。

但是,它可能指的是另一种异常处理方法的不同问题。引发新的异常。 (使用 raise; 可以避免这个问题,因为它会在 original 上下文中重新引发 the original 异常。)这样做是为了提供“更有意义的错误消息 - 类似于您的 示例 2

try

except
  raise EOtherError.Create('My Message');
end;

上面的问题是,当这个异常最终传播到应用程序处理程序时,您已经丢失了原来的 class;原始异常地址;和原始消息。这种方法通常会给用户带来更明显的错误:例如"Unable to open file filename" 但隐藏了可能对故障排除有用的信息。例如。是磁盘错误,是文件不是文件,是访问权限错误。

因此:无论何时处理错误(无论您使用何种方法)都要考虑的一件重要事情是:如果发生错误,是否有足够的信息来解决错误?