使用Abort来改善/简化某些情况下的代码

4
我之前进行过一次讨论:https://stackoverflow.com/a/42156860/937125,在那里我不太明白在某种情况下为什么Abort比调用Exit更好。我倾向于不在我的代码流中使用它。我认为这是一种不好的实践,会对代码流产生不良影响。但是,@David在评论中的说法让我想知道是否有些东西我没有理解到:

如果没有静默异常,当您在调用堆栈深处时,如何中止一个操作?例如,在具有10个深度调用堆栈的文件复制操作中,如何中止操作?这不正是异常设计的目的吗?当然,您可以在没有异常的情况下编写代码,但这将导致代码冗长且容易出错。

我无法想象这样的情况。有人能给我提供这样的代码/场景示例,并让我相信在上述情况下使用Abort真的是一件好事,并且“更加冗长和容易出错”。(3-4个深度调用堆栈就足以说明问题)

@Fero 方法A调用方法B,方法B调用方法C。方法C进行了早期退出。但是接下来B也需要这样做。A也是如此。 - David Heffernan
1
@David,抱歉如果我要求太多了,但您能否使用“最小、完整和可验证”的示例回答为什么Abort比Exit更好的问题,我认为这将为每个人分享更多光明。 - Alec
@Fero,方法是否返回值并不重要。重要的是,如果A或B在C返回后尝试执行操作。一旦用户取消,您不希望执行这些操作。你打算如何阻止它。 - David Heffernan
2
@DavidHeffernan,我了解“基于异常的代码与基于错误码的代码”的区别。我不确定这与Abort有什么关系,特别是在复制文件并在堆栈深度为10时取消的示例中。我理解BeforePost的示例,尽管我不会使用这样的代码。在OnBeforePost上调用Abort后数据集状态如何?如果在发布之前启动了事务,事务的状态是什么?这取决于幕后实现的方式,您需要全面了解它才能安全/确保在这种情况下使用Abort - kobik
2
好的,没问题。请继续你现在的方式。我不是来争论的,我是来学习的,因为我真的感觉自己没有理解到重点。所以你能否耐心地给一个 MCVE 的例子,比如 "例如,如何在一个10层的调用堆栈中中止文件复制操作?" 并且说明 Abort 是更好的解决方案。 - kobik
显示剩余9条评论
2个回答

7
我展示这个观点最简单的场景是这样的:
procedure MethodA;
begin
  MethodB;
  MethodC;
end;    

procedure MethodB;
begin
  // ... do stuff
end;

procedure MethodC;
begin
  // ... do stuff
end;

现在这样就可以了。现在假设MethodB要求用户输入一些内容,如果用户按下取消按钮,则不应继续进行任何工作。你可以像这样实现:

procedure MethodA;
begin
  if MethodB then
    MethodC;
end;    

function MethodB: Boolean;
begin
  Result := MessageDlg(...)=mrOK;
  if not Result then
    exit;
  // ... do stuff
end;

procedure MethodC;
begin
  // ... do stuff
end;

这个很好,但是想象一下,在真实的编码中,可能会有更深层次的嵌套。由MethodB返回的布尔值可能需要向上传递很多级别,这将变得繁琐。
或者考虑一下如果MethodB需要向其调用者返回一个值会发生什么情况。在这种情况下,原始代码可能如下所示:
procedure MethodA;
begin
  MethodC(MethodB);
end;    

function MethodB: string;
begin
  Result := ...;
end;

procedure MethodC(Value: string);
begin
  // ... do stuff with Value
end;

现在再考虑一下如果用户有机会取消会发生什么。我们如何从MethodB返回布尔值和字符串?使用一个输出参数来返回一个返回值?使用一个复合结构,比如记录来包装这两个值。后者显然涉及大量的样板文件,因此让我们探索前者。

procedure MethodA;
var
  Value: string;
begin
  if MethodB(Value) then
    MethodC(Value);
end;    

function MethodB(out Value: string): Boolean;
begin
  Result := MessageDlg(...)=mrOK;
  if not Result then
    exit;
  Value := ...;
end;

procedure MethodC(Value: string);
begin
  // ... do stuff with Value
end;

当然,您可以这样做,但这似乎是为了简化异常而设计的代码。此时,让我们考虑存在一个称为EAbort的静默异常,通过调用Abort引发,不会导致顶层异常处理程序显示消息。最后一点就是所谓的silent

现在代码变成了:

procedure MethodA;
begin
  MethodC(MethodB);
end;    

function MethodB: string;
begin
  if MessageDlg(...)<>mrOK then
    Abort;
  Result := ...;
end;

procedure MethodC(Value: string);
begin
  // ... do stuff with Value
end;

优点是MethodA不需要担心取消操作。如果调用栈更深,MethodA在顶部和用户输入点之间的任何方法都不需要知道有关取消操作的内容。
另一个好处是MethodB可以保留其自然签名。它返回一个字符串。如果出现传统异常或用户取消等错误,则会抛出异常。
这个非常简单的例子并没有比不使用Abort的前一个例子更令人信服。但是,想象一下如果MethodB在调用栈中深达4或5层会是什么样子?
我绝对不是说Abort应该总是代替exit。我的观点是两者都有适用的场景。当用户选择取消操作,并且你不希望在当前事件处理程序中进行进一步处理时,Abort很棒。此外,由于用户明确选择取消,因此无需向他们呈现其他UI。您不需要消息框告诉用户他们已取消,因为他们已经知道了。

1
抱歉,David,这让我更加困惑了。有什么方法可以阻止静默异常进一步扩散吗?(我知道你说MethodA是顶层,但实际上它不一定是)。难道你不需要在要跳出的顶层使用try except结构吗?也许在这个人工示例中不需要,但肯定会使事情更清晰吧? - Dsm
在VCL框架中,有一个顶层异常处理程序来捕获和忽略EAbort异常。显然你需要有某种方式来做到这一点。如果不能信任异常会在正确的位置被捕获和处理,整个异常概念就会崩溃。 - David Heffernan
是的,David,我知道。我觉得你没有理解我的意思。如果MethodA是你想要停止的地方,而MethodA在调用堆栈的中间位置,那么有什么能阻止它一直返回到VCL框架呢?我只是觉得这个问题可以更清晰明了。 - Dsm
@Dsm 我假设读者已经了解异常处理的工作原理。我认为详细解释这一点并不合适。你还有疑问吗? - David Heffernan
不,我并不困惑。我也不希望你比你已经解释的更详细地解释。只是认为在你的示例代码中加入一个try except子句会让未来的读者更清楚地理解。尤其是在快速阅读时,有东西比没有东西更容易理解。 - Dsm
@Dsm 实际上,我认为在不需要的地方使用except是一个常见的错误。通常对于用户交互,你希望它向上浮动。 - David Heffernan

4
假设你的程序正在进行一个长时间的操作,可能是在一个单独的线程中,或者(尽管这不被赞成)调用了Application.ProcessMessages。现在,你希望用户能够以安全的方式中止该操作(即:所有资源都得到清理,数据处于一致的状态等)。因此,UI设置了一个标志,在你的代码中定期检查该标志。如果已经设置,你可以调用Abort或显式地引发EAbort。这将导致所有精心编写的try / except / finally块被执行,并确保操作的中止是安全的。
// in the main thread:
procedure TMyProgressDialog.b_AbortClick(Sender: TObject);
begin
  if AskUserIfHeIsSure then begin
    gblAbortedFlag := true;
    b_Abort.Enabled := false;
    b_Abort.Caption := _('Aborting');
  end;
end;

// call this repeatedly during the lenghty operation:
procecdure CheckAborted;
begin
  // If you are in the main thread, you might want to call
  // Application.ProcessMessages;
  // here. If not, definitely don't.
  if gblAbortedFlag then
    Abort;
end;

当然,也可以使用其他异常来做到这一点,但是我想不出其他安全退出深度调用堆栈的方法,而不必编写大量的if和exit语句。

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