在某些情况下使用 Abort to improve/simplify 代码

Using Abort to improve/simplify code in some situations

前几天我讨论过: 我不太明白为什么在那种情况下 Abort 比调用 Exit 更好。我倾向于不在我的代码流中使用它。我认为这是一种不好的做法,不利于代码流。 但是@David 在评论中的声明让我想知道我是否遗漏了什么:

Without a silent exception, how would you abort an operation when deep down the call stack. For instance how would you abort a file copy operation with a 10 deep call stack? Isn't that exactly what exceptions are designed for? Sure you can code it without exceptions but it is much more verbose and error prone.

我无法想象这样的情况。谁能给我一个这样的 code/scenario 的例子,并让我相信在上面的例子中 Abort"much more verbose and error prone" 确实是一件好事。 (3-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;

当然,这可以通过不同的异常来完成,但我想不出任何其他方法来安全地退出深度调用堆栈,而无需编写大量 ifs 和退出程序。

最能说明我的观点的场景如下:

procedure MethodA;
begin
  MethodB;
  MethodC;
end;    

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

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

这样就好了。现在假设 MethodB 要求用户输入一些内容,如果用户按下 Cancel 按钮,则不应执行进一步的操作。你可以这样实现:

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 编辑的布尔值 return 可能需要向上传递很多级别。这会变得很麻烦。

或者考虑如果 MethodB 需要 return 一个值给它的调用者会发生什么。在那种情况下,原始代码可能是这样的:

procedure MethodA;
begin
  MethodC(MethodB);
end;    

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

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

现在再次考虑如果用户有机会取消会发生什么。我们如何 return 来自 MethodB 的布尔值和字符串?对 return 值之一使用输出参数?使用像记录这样的复合结构来包装两个值。后者显然涉及很多样板文件,所以让我们探讨一下前者。

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;

当然你可以做到这一点,但这开始看起来像异常旨在简化的那种代码。在这一点上,让我们考虑通过调用 Abort 引发的静默异常 EAbort 的存在,它不会导致顶级异常处理程序显示消息。最后一点就是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 之间的方法 none 将需要了解有关取消的任何信息。

另一个好处是 MethodB 可以保留其自然签名。它 return 是 string。如果失败,无论是来自更传统的异常,还是来自用户取消,都会抛出异常。

这个非常简单的例子并不比上一个不使用 Abort 的例子更有说服力。但是想象一下,如果 MethodB 在调用堆栈中有 4 或 5 个深度,代码会是什么样子?


我绝对不是说 Abort 应该总是用来代替 exit。我的信念是两者都有自己的位置。 Abort 的亮点是当用户选择取消操作并且您不希望在当前事件处理程序中进行任何更多处理时。此外,由于用户明确选择取消,因此无需再向他们显示 UI。您不需要一个消息框来告诉用户他们取消了,他们已经知道了。