Using Abort to improve/simplify code in some situa

2019-04-10 10:07发布

I had a discussion the other day: https://stackoverflow.com/a/42156860/937125 where I didn't quite understand why an Abort was better than calling Exit in that situation. I tend not to use it in my code flow. I consider it a bad practice and bad for code flow. but @David's statement in the comments made me wonder if maybe I was missing something:

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.

I can't imagine such situation. Can someone give me an example of such code/scenario, and convince me that Abort in the above case is really a good thing and "much more verbose and error prone". (3-4 deep call stack is enough to illustrate)

标签: delphi abort
2条回答
成全新的幸福
2楼-- · 2019-04-10 10:31

Assume your program is doing a lengthy operation either in a separate thread or (even though it's frowned upon) calling Application.ProcessMessages. Now, you want the user to be able to abort that operation in a safe manner (that is: All resources are cleaned up, the data is in a consistent state etc.). So, the UI sets a flag somewhere and in your code you periodically check for that flag. If it is set, you call Abort or explicitly raise EAbort. This will cause all your carefully crafted try / except / finally blocks to be execute and making sure aborting the operation is safe.

// 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;

Of course this could be done with a different exception, but I can't think of any other way to safely exit from a deep call stack without having to program lots of ifs and exits.

查看更多
欢心
3楼-- · 2019-04-10 10:47

The simplest scenario that illustrates my point is like so:

procedure MethodA;
begin
  MethodB;
  MethodC;
end;    

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

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

That's fine as it is. Now suppose that MethodB asks the user for some input, and if the user presses the Cancel button, that no further work should be carried out. You could implement that like this:

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;

That works fine, but imagine that you in the real world code, there was deeper nesting. The boolean returned by MethodB might need to be passed on up a great many levels. This would become cumbersome.

Or consider what happens if MethodB needs to return a value to its caller. In that scenario the original code might be like so:

procedure MethodA;
begin
  MethodC(MethodB);
end;    

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

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

Now once more consider what happens if the user gets a chance to cancel. How can we return both a boolean and a string from MethodB? Using an out parameter for one of the return values? Using a compound structure like a record to wrap both values. The latter obviously involves lots of boilerplate so let us explore the former.

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;

For sure you can do this, but this is beginning to look like the sort of code that exceptions were designed to simplify. And at this point, let us consider the existence of a silent exception, EAbort, raised by calling Abort, that does not result in a message being shown by the top level exception handler. That last point is what is meant by silent.

Now the code becomes:

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;

The advantage is that MethodA does not need to worry about cancellation. And if the call stack was deeper, none of the methods between MethodA at the top, and MethodB at the point of user input, would need to know anything about cancellation.

A further benefit is that MethodB can retain its natural signature. It returns a string. In case of failure, either from a more traditional exception, or from user cancellation, an exception is thrown.

This very simple example isn't that much more compelling than the previous one that does not use Abort. But imagine what the code would look like if MethodB were 4 or 5 deep in the call stack?


I am absolutely not saying that Abort should always be used in place of exit. My belief is that both have their place. Where Abort shines is when the user opts to cancel an operation and you don't want any more processing to take place in the current event handler. Furthermore, since the user expressly opted to cancel, no further UI needs to be presented to them. You don't need a message box telling the user that they cancelled, they already know that.

查看更多
登录 后发表回答