【问题标题】:Delphi Error handling : Raise vs Exit on try...except...endDelphi 错误处理:在 try...except...end 时引发与退出
【发布时间】:2017-07-23 12:49:42
【问题描述】:

在 try except 时调用 Exit 是否安全? 或者我应该改为调用 raise 吗?

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

我读到最好保留应用程序堆栈或队列或类似的东西。调用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

【问题讨论】:

  • 安全。但这里很难给出建议。如果您进行异常驱动编程,请引发异常。

标签: delphi-10.1-berlin delphi


【解决方案1】:

很少有替代选项(A 与 B)可以客观地评估,因为其中一个“总是比另一个更好”。这就是为什么正确理解它们之间的差异和含义很重要的原因。

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

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

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

您的 2 个示例之间的主要区别是:

  • 示例 1 能够在调用堆栈上报告故障状态。
  • 示例 2 隐藏了此信息,因为异常处理程序吞下了异常。

示例 1也会跳过 [B] 和 [D] 代码。但是,示例 2 将执行 [B] 和 [D] 代码。当您了解这种差异后,您就有权决定是否应该执行 [B] 和 [D]

但是,我经常怀疑CallDatabaseMethod 未能正确地做所有事情的事实表明 [B] 和 [D] 不应该叫。例如。假设数据库方法更新客户账户数据并且[B]和[D]执行与发送最新语句相关的操作。 您可能不想在更新失败时发送声明!

也就是说,如果您的方法可以被视为“成功完成”尽管异常,那么无论如何吞下异常是完全可以接受的。例如。假设您有一个“添加行”的方法,其后置条件就是该行必须存在于数据库中。然后,如果您的数据库返回 PK 违规,则显然该行确实存在在这种情况下,吞下异常是非常有意义的。


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

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

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 的工作方式相同。它确实有其优点和缺点:

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

我建议最好的方法是确定哪些类型的错误可以被视为“正常”,并确保使用明确的错误结果而不是异常来处理这些错误。当然,上述 1 的实例是主要候选对象。


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

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

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

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


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

我读到最好保留应用程序堆栈或队列或类似的东西。

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

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

try

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

上面的问题是,当这个异常最终传播到应用程序处理程序时,你就失去了原来的类;原始异常地址;和原始消息。这种方法通常会给用户带来更清晰的错误:例如“无法打开文件 filename”,但隐藏了可能对故障排除有用的信息。例如。是磁盘错误,是文件不是文件,是访问权限错误。

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

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

    【讨论】:

    • 这个尝试除了发生在按钮点击之外。我想将一些数据保存到数据库中,并在进程失败时回滚。除了按钮单击事件之外,这个 try except 块是唯一的尝试,所以我不需要处理外部尝试。从您的回答看来,我可以只使用 MessageDlg 和 Exit 对吗?顺便说一句,当我尝试使用 MessageDlg 但没有退出来跟踪进程时,跟踪从按钮单击事件进入 Delphi 的内部源,是否可以退出并跳过这些步骤?
    • 在未来的某个时候,您可能希望重新安排代码以支持不同的使用模式。在这种情况下,此代码上方很可能有一个调用堆栈。我猜测您想中止整个操作,在这种情况下引发异常并让应用程序异常处理程序处理这是最好的方法。
    • 两个版本的代码都可以,根据我的回答中的附带条件。我想我已经说过了......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多