Some time ago, I blogged about writing smart pointers (i.e. reference-counted auto-destruction) in Delphi. While having dinner with some of the speakers at the EKON 12 conference I attended last week, a more fluent interface for using smart pointers in Delphi occurred to me.
I'm using the same TSmartPointer<T> class that I started out with in the previous article, though I've renamed it TObjectHandle<T>. The main tricks I'm pointing out here are (1) to use method references to avoid having to use the Value property all the time, and (2) to use aliases at the point of class definition to make construction slightly more palatable.
So, here's my new TObjectHandle<T> class; the main change, apart from the name, is a new method called Wrap:
type
TObjectHandle<T: class> = record
private
FValue: T;
FLifetimeWatcher: IInterface;
public
constructor Create(const AValue: T);
property Value: T read FValue;
class operator Implicit(const AValue: T): TObjectHandle<T>;
class function Wrap(const AValue: T): TFunc<T>; static;
end;
The implementation of the new method is pretty simple too:
class function TObjectHandle<T>.Wrap(const AValue: T): TFunc<T>;
var
h: TObjectHandle<T>;
begin
h := AValue;
Result := function: T
begin
Result := h.Value;
end;
end;
The capture of the h local variable will mean that the handle will be kept alive as long as the method reference constructed from the anonymous method is kept alive.
Here it is in use, as two versions, so that the usage difference can be seen. This is also where the additional lubrication of declaring aliases comes in. I start out with a little TCanary class which can keep track of destruction, and has a Name property to demo the fluency of the technique:
type
TCanary = class
private
FName: string;
public
destructor Destroy; override;
property Name: string read FName write FName;
end;
OHCanary = TObjectHandle<TCanary>;
HCanary = TFunc<TCanary>;
The destructor prints out the name of the canary when it is destroyed. The two aliases represent an Object Handle for TCanary and a Handle for TCanary respectively. The fluent technique relies on both; the second is used for smart pointer locations and the first for smart pointer construction. There is a tradeoff involved in the technique, between construction fluency and usage fluency:
procedure Test1;
var
canary: OHCanary;
begin
// easy construction (implicit operator)
canary := TCanary.Create;
// but cumbersome access - always need Value accessor
canary.Value.Name := 'Test1 canary';
end;
The new style has slightly worse construction, but better actual use:
procedure Test2;
var
canary: HCanary;
begin
// cumbersome constructor
canary := OHCanary.Wrap(TCanary.Create);
// but much nicer access
canary.Name := 'Test2 canary';
end;
Without having to access everything by prefixing every access with .Value, a lot of fluency is gained, IMHO.
To summarize, here's the entire ObjHandle.pas unit:
unit ObjHandle;
interface
uses SysUtils;
type
TObjectHandle<T: class> = record
private
FValue: T;
FLifetimeWatcher: IInterface;
public
constructor Create(const AValue: T);
property Value: T read FValue;
class operator Implicit(const AValue: T): TObjectHandle<T>;
class function Wrap(const AValue: T): TFunc<T>; static;
end;
TObjectHandleArray<T: class> = array of TObjectHandle<T>;
procedure MakeDestroyer(Obj: TObject; out Result: IInterface);
implementation
{ TLifetimeWatcher }
type
TLifetimeWatcher = class(TInterfacedObject)
private
FProc: TProc;
public
constructor Create(const AProc: TProc);
destructor Destroy; override;
end;
constructor TLifetimeWatcher.Create(const AProc: TProc);
begin
FProc := AProc;
end;
destructor TLifetimeWatcher.Destroy;
begin
if Assigned(FProc) then
FProc;
inherited;
end;
procedure MakeLifetimeWatcher(out Result: IInterface; const AProc: TProc);
begin
Result := TLifetimeWatcher.Create(AProc);
end;
procedure MakeDestroyer(Obj: TObject; out Result: IInterface);
begin
Result := TLifetimeWatcher.Create(procedure
begin
Obj.Free;
end);
end;
{ TObjectHandle<T> }
constructor TObjectHandle<T>.Create(const AValue: T);
begin
FValue := AValue;
MakeDestroyer(FValue, FLifetimeWatcher);
end;
class operator TObjectHandle<T>.Implicit(const AValue: T): TObjectHandle<T>;
begin
Result := TObjectHandle<T>.Create(AValue);
end;
class function TObjectHandle<T>.Wrap(const AValue: T): TFunc<T>;
var
h: TObjectHandle<T>;
begin
h := AValue;
Result := function: T
begin
Result := h.Value;
end;
end;
end.
And here's the entire demo program:
{$apptype console}
uses SysUtils, ObjHandle;
type
TCanary = class
private
FName: string;
public
destructor Destroy; override;
property Name: string read FName write FName;
end;
OHCanary = TObjectHandle<TCanary>;
HCanary = TFunc<TCanary>;
destructor TCanary.Destroy;
begin
Writeln(FName, ' died.');
end;
procedure Test1;
var
canary: OHCanary;
begin
// easy construction (implicit operator)
canary := TCanary.Create;
// but cumbersome access - always need Value accessor
canary.Value.Name := 'Test1 canary';
end;
procedure Test2;
var
canary: HCanary;
begin
// cumbersome constructor
canary := OHCanary.Wrap(TCanary.Create);
// but much nicer access
canary.Name := 'Test2 canary';
end;
begin
Test1;
Test2;
end.