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
" 但隐藏了可能对故障排除有用的信息。例如。是磁盘错误,是文件不是文件,是访问权限错误。
因此:无论何时处理错误(无论您使用何种方法)都要考虑的一件重要事情是:如果发生错误,是否有足够的信息来解决错误?
在 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
" 但隐藏了可能对故障排除有用的信息。例如。是磁盘错误,是文件不是文件,是访问权限错误。
因此:无论何时处理错误(无论您使用何种方法)都要考虑的一件重要事情是:如果发生错误,是否有足够的信息来解决错误?