Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding nested try...finally blocks in Delphi

I had this idea this morning on avoiding nested try finally blocks like the following

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
begin
  T1 := TTestObject.Create('One');
  try
    T2 := TTestObject.Create('Two');
    try
      T3 := TTestObject.Create('Three');
      try
        //A bunch of code;
      finally
        T3.Free;
      end;
    finally
      T2.Free;
    end;
  finally
    T1.Free;
  end;
end;

By taking advantage of the automated reference counting of interfaces, I have come up with

Type  
  IDoFinally = interface
    procedure DoFree(O : TObject);
  end;

  TDoFinally = class(TInterfacedObject, IDoFinally)
  private
    FreeObjectList : TObjectList;
  public
    procedure DoFree(O : TObject);
    constructor Create;
    destructor Destroy; override;
  end;

//...

procedure TDoFinally.DoFree(O : TObject);
begin
  FreeObjectList.Add(O);
end;

constructor TDoFinally.Create;
begin
  FreeObjectList := TObjectList.Create(True);
end;

destructor TDoFinally.Destroy;
begin
  FreeObjectList.Free;
  inherited;
end;

So that the previous block of code becomes

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
  DoFinally : IDoFinally;
begin
  DoFinally := TDoFinally.Create;
  T1 := TTestObject.Create('One');
  DoFinally.DoFree(T1);
  T2 := TTestObject.Create('Two');
  DoFinally.DoFree(T2);
  T3 := TTestObject.Create('Three');
  DoFinally.DoFree(T3);
  // A Bunch of code;
end;

My question is: does this work or have I overlooked something?

To me this looks pretty cool, and makes the code a bit easier to read with the reduced amount of nesting. It could also be extended to store a list of anonymous methods to run to do things such as close files, queries, etc...

like image 962
Alister Avatar asked Aug 28 '13 19:08

Alister


1 Answers

Yes, it works.

Perhaps the only thing different between the nested try-finally blocks of the original code and the technique of using a reference-counted object to manage the lifetimes of other objects is what happens if there's a problem destroying any of the objects. If there's an exception while any object is being destroyed, the nested try-finally blocks will ensure that any remaining objects will still get freed. The TObjectList in your TDoFinally doesn't do that; if any item in the list can't be destroyed, any subsequent items in the list will be leaked.

In practice, that's not really a problem, though. No destructor should ever throw an exception. If it does, there's not really any way to recover from it anyway, so it doesn't matter if anything leaks because of it. Your program should terminate momentarily anyway, so having a tidy cleanup routine is of little importance.

Incidentally, the JCL already offers the ISafeGuard and IMultiSafeGuard interfaces for managing local objects' lifetimes. For example, you could rewrite your code like this:

uses JclSysUtils;

procedure DoSomething;
var
  T1, T2, T3: TTestObject;
  G: IMultiSafeGuard;
begin
  T1 := TTestObject(Guard(TTestObject.Create('One'), G));
  T2 := TTestObject(Guard(TTestObject.Create('Two'), G));
  T3 := TTestObject(Guard(TTestObject.Create('Three'), G));
  // A Bunch of code;
end;

That library doesn't address exceptions in destructors, either.

like image 51
Rob Kennedy Avatar answered Oct 04 '22 10:10

Rob Kennedy