Can someone provide an explanation of a weak reference in Delphi?
I noticed that the concept is often mentioned in some library/framework source code I scrutini
In a most general case a strong reference
controls the lifetime of a referenced instance while a weak reference
does not. The term weak reference
can be used in context of a garbage collector, reference counted interfaces or common objects.
For example, a Delphi form hold the references to all its controls; these references can be called strong because when a form is destroyed its controls are also destroyed. On the other hand, a Delphi form's control has a reference to a form it belongs to. This reference can be call weak because it does not controls a form's lifetime in any way.
Instances that reference each other by interface references keep each other alive in a reference count based interface implementation.
A weak reference is used to break the "keep each other alive" bear hug. This is done by declaring one reference as a pure pointer to circumvent the reference counting mechanism.
IFriend = Interface(IInterface)
end;
TFriend = class(TInterfacedObject, IFriend)
private
FFriend: IFriend;
end;
var
Peter: IFriend;
John: IFriend;
begin
Peter := TFriend.Create;
John := TFriend.Create;
Peter.Friend := John;
John.Friend := Peter;
end;
Even when Peter and John go out of scope, their instances are kept around because their mutual reference keeps their refcount from dropping to zero.
The problem is more commonly found in composite patterns (parent - child relationships) where the child has a back reference to the parent:
ISomething = Interface(IInterface)
end;
TSomething = class(TInterfacedObject, ISomething)
end;
TParent = class(TSomething)
FChildren: TInterfacedList;
end;
TChild = class(TSomething)
FParent: ISomething;
end;
Again, parent and child can keep eachother around because their mutual reference keeps their refcount from dropping to zero.
This is solved with a weak reference
:
TChild = class(TSomething)
FParent: Pointer;
end;
By declaring the FParent as a "pure" pointer the reference counting mechanism doesn't come into play for the back reference to the parent. When a parent goes out of scope, its reference count now can drop to zero because its children no longer keep its ref count above zero.
Note This solution does require careful attention to lifetime management. Children can be kept alive beyond the life time of the parent when something on the "outside" of these classes keeps a reference to a child. And this can lead to all sorts of interesting AV's when the child assumes the parent reference always points to a valid instance. If you need it, make sure that when the parent goes out of scope, it makes the children nil their back references before it nils its own references to its children.
See also
Automatic Reference Counting in Delphi Mobile Compilers
which includes documentation of the new [weak]
attribute:
Another important concept for ARC is the role of weak references, which you can create by tagging them with [weak] attribute.
By default in Delphi, all references are either:
pointer
and class
instances;integer, Int64, currency, double
or record
(and old deprecated object
or shortstring
);string, widestring, variant
or a dynamic array);interface
instances;The main issue with strong reference counting is the potential circular reference problem. This occurs when an interface
has a strong reference to another, but the target interface
has a strong pointer back to the original. Even when all other references are removed, they still will hold on to one another and will not be released. This can also happen indirectly, by a chain of objects that might have the last one in the chain referring back to an earlier object.
See the following interface definition for instance:
IParent = interface
procedure SetChild(const Value: IChild);
function GetChild: IChild;
function HasChild: boolean;
property Child: IChild read GetChild write SetChild;
end;
IChild = interface
procedure SetParent(const Value: IParent);
function GetParent: IParent;
property Parent: IParent read GetParent write SetParent;
end;
The following implementation will definitively leak memory:
procedure TParent.SetChild(const Value: IChild);
begin
FChild := Value;
end;
procedure TChild.SetParent(const Value: IParent);
begin
FParent := Value;
end;
In Delphi, most common kind of reference-copy variables (i.e. variant, dynamic array or string) solve this issue by implementing copy-on-write. Unfortunately, this pattern is not applicable to interface, which are not value objects, but reference objects, tied to an implementation class, which can't be copied.
Note that garbage collector based languages (like Java or C#) do not suffer from this problem, since the circular references are handled by their memory model: objects lifetime are maintained globally by the memory manager. Of course, it will increase memory use, slowdown the process due to additional actions during allocation and assignments (all objects and their references have to be maintained in internal lists), and may slow down the application when garbage collector enters in action.
One common solution with languages with no garbage-collection (like Delphi) is to use Weak pointers, by which the interface is assigned to a property without incrementing the reference count. In order to easily create a weak pointer, the following function could be used:
procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface);
begin
PPointer(aInterfaceField)^ := Pointer(aValue);
end;
Therefore, it could be used as such:
procedure TParent.SetChild(const Value: IChild);
begin
SetWeak(@FChild,Value);
end;
procedure TChild.SetParent(const Value: IParent);
begin
SetWeak(@FParent,Value);
end;
You can try to read my blog post about weak references in Delphi - and its associated source code: we have implemented direct weak reference, and "zeroing" weak reference interface handling from Delphi 6 up to XE2.
In fact, in some cases, you'll need to set the interface weak fields to nil
, if you release the reference instance before its child, to avoid any Access Violation issue. This is called "Zeroing Weak pointers", and what Apple implemented with the ARC model, and that we tried to implement in Delphi.