Using interfaces and reference counting in Delphi works great for the most part. Its a feature I use a lot,
I'm a big fan of using interfaces to tightly control what parts of a class a consumer has access to. But, there
is one big achillies heel with reference counting in Delphi, you cannot keep circular references,
at least not easily, without causing memory leaks.
Consider this trivial example :
IChild = interface;
IParent = interface
['{62DC70E1-8D82-4012-BF01-452EB0F7F45A}']
procedure AddChild(const AChild : IChild);
end;
IChild = interface
['{E1DB1DA0-55D6-408E-8143-072CA433412D}']
end;
TParent = class( TInterfacedObject, IParent )
private
FChild : IChild;
procedure AddChild(const AChild : IChild);
public
destructor Destroy; override;
end;
TChild = class( TInterfacedObject, IChild )
private
FParent : IParent;
public
constructor Create( AParent : IParent );
destructor Destroy; override;
end;
implementation
constructor TChild.Create(AParent: IParent);
begin
inherited Create;
FParent := AParent;
AParent.AddChild(Self);
end;
destructor TChild.Destroy;
begin
FParent := nil;
inherited;
end;
procedure TParent.AddChild(const AChild: IChild);
begin
FChild := AChild;
end;
destructor TParent.Destroy;
begin
if Assigned( FChild ) then
FChild := nil;
inherited;
end;
procedure Test;
var
MyParent : IParent;
MyChild : IChild;
begin
MyParent := TParent.Create;
MyChild := TChild.Create(MyParent);
MyChild := nil;
MyParent := nil;
end;
Both parent and child are now orphaned and we have no reference to them and no way to free them! Ideally, the parent would
control the life of the child, but the child would not control the life parent.
So how can we get around this? Well a technique that I have used a lot in the past is to not hold a reference to the
parent in the child, but rather just a pointer to the parent.
TChild = class(TInterfacedObject,IChild)
private
FParent : Pointer;
...
end;
constructor TChild.Create(AParent : IParent);
begin
FParent := Pointer(AParent);
end;
function TChild.GetParent : IParent;
begin
result := IParent(FParent); // if the parent has been released the we are passing out a bad reference!
// a nil reference would be preferable as it's easy to check.
end;
This works well for the most part, but it does have the potential for access voilations if you do not understand or at least know how the child is referencing the parent.
For example :
var child : IChild;
parent : IParent
begin
parent := TParent.Create;
child := TChild.Create(parent):
parent := nil; //parent will now be freed, since nothing has a reference to it.
.......
parent := child.GetParent; //kaboom
end;
One of my collegues kindly pointed out that C# doesn't suffer from this problem and he uses circular references all the
time without even thinking about it. While discussing this, he mentioned the WeakReference class in .NET.
It basically allows you to hold a reference to a object without affecting it's lifecycle (ie, not influencing when it will be garbage collected).
I figured there must be a way to do this in Delphi, and so set about creating a WeakReference class for Delphi.
I wasn't able to find a reliable way to do this with any old TInterfacedObject descendant, however by creating a TWeakReferencedObject class and the use of generics on
Delphi 2010 I did manage to implement something that works well and is not too cumbersome. Lets take a look at our Child/Parent example using
a weak reference.
The important part in this is the use of the WeakReference to the parent in the Child class. So instead of declaring
FParent : IParent;
we have
FParent : IWeakReference<IParent>;
We create it using
FParent := TWeakReference<IParent>.Create(parent); //value is an IParent instance
This is how our TChild.GetParent /SetParent methods look now :
function TChild.GetParent: IParent;
begin
if FParent <> nil then
result := FParent.Data as IParent
else
result := nil;
end;
procedure TChild.SetParent(const value: IParent);
begin
if (FParent <> nil) and FParent.IsAlive then
FParent.Data.RemoveChild(Self);
FParent := nil;
if value <> nil then
FParent := TWeakReference<IParent>.Create(value);
end;
Note the use of the IsAlive property on our weak reference, this tells us whether the referenced object is still available, and provides a
safe way to get a concrete reference to the parent.
I still think this is something that could be solved in a better way by the delphi compiler/vcl guys n girls.
Hopefully someone will find this useful, the code is available for download here - Updated Sunday 28/3/2010
Feedback welcolme, I'm about to start making extensive use of
this code, so if you see any holes then please do let me know!