Some C++ aficionados point at the ability, in C++, to create an object such that the creation allocates some kind of resource, and the destruction frees the resource, with much glee. The idiom is RAII: Resource Acquisition Is Initialization. The technique can be a useful, albeit somewhat opaque, way of acquiring a resource for the duration of a block of code and relinquishing the resource upon exiting the block. It's C++'s equivalent of Delphi / Java / C# / SEH's try / finally blocks (though of course C#'s "using" statement is an even closer analogue).
Delphi has a similar mechanism in the form of deterministic reference counting for interfaces. This can be (ab)used to functionally implement almost exactly the same technique as C++. The degree to which it is useful, however, is moderated by readability issues; RAII doesn't necessarily employ a visible nesting, and rather depends on the implied nesting of the scope in which it's used.
Anyway, here's a way of implementing RAII in Delphi, for people to whom the technique may not have occurred yet. It relies on defining a general-purpose class which implements an interface, and whose death will be triggered by scope exit:
type
TScopeExitNotifier = class(TInterfacedObject)
private
FProc: TProc;
public
constructor Create(const AProc: TProc);
destructor Destroy; override;
end;
constructor TScopeExitNotifier.Create(const AProc: TProc);
begin
FProc := AProc;
end;
destructor TScopeExitNotifier.Destroy;
begin
if Assigned(FProc) then
FProc;
inherited;
end;
function MakeScopeExitNotifier(const AProc: TProc): IInterface;
begin
Result := TScopeExitNotifier.Create(AProc);
end;
I would generally recommend hiding the actual class in the implementation-section of a unit - the functional interface of MakeScopeExitNotifier ought to be sufficient. Using this function, we can now implement a one-liner RAII equivalent of the following code:
Writeln('Disable controls on ', {ASurface});
try
// (user code)
finally
Writeln('Enable controls on ', {ASurface});
end;
Of course, I'm using strings etc. for simplicity, so that the demo translation is compilable. Here is the definition of the function which will be called to form the one-liner:
function DisableEnableControls(const ASurface: string): IInterface;
begin
Writeln('Disable controls on ', ASurface);
Result := MakeScopeExitNotifier(procedure
begin
Writeln('Enable controls on ', ASurface);
end);
end;
And here it is in action:
procedure P;
begin
DisableEnableControls('Foo');
DisableEnableControls('Bar');
Writeln('Controls on Foo and Bar are disabled now');
end;
Because of the way Delphi's deterministic reference counting and destruction works, Foo and Bar will be released in reverse order of construction, as expected for RAII. Functions that return an interface value in Delphi are implemented behind the scenes as procedures that take an out Result parameter; these are in effect hidden variables that keep the return values alive until the scope is exited.
The output of the program is as follows:
Disable controls on Foo Disable controls on Bar Controls on Foo and Bar are disabled now Enable controls on Bar Enable controls on Foo
Of course, it's not necessarily clear what DisableEnableControls(string) would do, and how it protects the scope. That's why I wouldn't necessarily recommend using this technique, except to counter C++-ite arguments. Using anonymous methods directly would seem to be a more readable approach in general, something along these lines:
procedure DisableEnableControls(const ASurface: string; const ACB: TProc);
begin
Writeln('Disable controls on ', ASurface);
try
if Assigned(ACB) then
ACB;
finally
Writeln('Enable controls on ', ASurface);
end;
end;
procedure P;
begin
DisableEnableControls('Foo', procedure
begin
DisableEnableControls('Bar', procedure
begin
Writeln('Controls are disabled now.');
end);
end);
end;
Output is the same as earlier. The extra complexity / verbosity of this abstraction over and above a literal try/finally would of course be more justified if the logic in the place of Enable and Disable sections were more involved, such as possibly setting up and rolling back transactions, etc.
However, the functionality of MakeScopeExitNotifier() routine is actually useful in the context of anonymous methods, because by capturing a variable containing the return value, you can get informed as to when the scope that created your anonymous method dies. This can be useful to e.g. free any objects that were allocated when the anonymous method was created:
function FreeOnExit(const Args: array of TObject): IInterface;
var
toFree: TArray<TObject>;
begin
// copy open array to toFree because open arrays can't be captured
SetLength(toFree, Length(Args));
Move(Args[0], toFree[0], Length(Args) * SizeOf(Args[0]));
Result := MakeScopeExitNotifier(procedure
var
i: Integer;
begin
Writeln('FreeOnExit is freeing objects');
for i := 0 to High(toFree) do
toFree[i].Free;
end);
end;
procedure Use(const x: IInterface);
begin
end;
function Q: TProc;
var
onDie: IInterface;
obj: TObject;
begin
obj := nil;
onDie := FreeOnExit([obj]);
Writeln('Allocating an object');
obj := TObject.Create;
Result := procedure
begin
Use(onDie); // cause onDie to be captured
Writeln(Format(
'Inside callback, obj still alive: %p: %s',
[Pointer(obj), obj.ToString]));
end;
end;
procedure P;
var
r: TProc;
begin
Writeln('Before Q');
r := Q();
Writeln('Invoking result of Q');
r;
Writeln('Clearing result of Q');
r := nil;
Writeln('Result is cleared');
end;
The output is something like:
Before Q Allocating an object Invoking result of Q Inside callback, obj still alive: 00970D20: TObject Clearing result of Q FreeOnExit is freeing objects Result is cleared
This technique, capturing a variable of an interface or other deterministically lifetime-managed type, is essential for flexibility in cleaning up any resources that an anonymous method needs to for its lifetime. Note, however, that there can be a gotcha: the following alternative, using MakeScopeExitNotifier directly, will not work:
// <BROKEN CODE DO NOT USE>
function Q: TProc;
var
onDie: IInterface;
begin
onDie := MakeScopeExitNotifier(procedure
begin
Writeln('died!');
end);
Result := procedure
begin
Use(onDie);
end;
end;
// </BROKEN CODE DO NOT USE>
The reason it's broken is that the anonymous method that's handed off to MakeScopeExitNotifier will itself keep the scope alive. So in order to make use of MakeScopeExitNotifier for this particular purpose, the anonymous method passed to it needs to be in a different scope.