Answer: When it is merely the container for an interface.
After a long series of observation and opinion pieces, I thought it about time I posted something a little more technical, so here we go.
I think it is a well known practice to store references to objects in the Tag, Data or Objects members of various VCL classes and controls. This is often used for example with TListBox, TComboBox, TListView and TTreeView – among many others – to hold a reference to an object that an item in one of those controls refers to:
var cust: TCustomer; begin // do something to acquire a cust reference... : // ...and add it to a listbox: lbCustomers.Items.AddObject(cust.Name, cust); end;
The object can then be later retrieved by simply casting the corresponding entry in the listbox Objects property:
var selectedCust: TCustomer; begin // Get the selected customer: selectedCust := TCustomer(lbCustomers.Items.Objects[lbCustomers.ItemIndex]); end;
selectedCust := lbCustomers.Items.Objects[lbCustomers.ItemIndex] as TCustomer;
Incidentally, this is an example of one of the few (very few) cases where I consider with to be very helpful, entirely safe and unlikely to fall foul of the debugger shortcomings (i.e. unlikely to require debugging and/or the debugger shortcomings easily circumvented since there is an explicit symbol – lbCustomers – that can be used to evaluate any values explicitly that the debugger is unable to resolve implicitly itself. I wish the people who spend so much time arguing against the use of with would put as much energy into asking for the debugger to be fixed – the compiler and the IDE have no trouble with this construct, the debugger shouldn’t either):
with lbCustomers do selectedCust := TCustomer(Items.Objects[ItemIndex]);
with lbCustomers do selectedCust := Items.Objects[ItemIndex] as TCustomer;
But I digress.
The question relevant to this post is: What if the object reference you wish to store in one of the properties (Tag, Data, Objects etc) isn’t an object reference but is instead an interface reference?
Casting an interface reference into a suitable form for storage in a non-interface typed property is both messy and risky. An alternative technique is to “wrap” the interface inside an object:
type TInterface = class // A naive implementation of an interface wrapper private fRef: IUnknown; public constructor Create(const aRef: IUnknown); property Ref: IUnknown read fRef; end; constructor TInterface.Create(const aRef: IUnknown); begin inherited Create; fRef := aRef; end;
NOTE: By design this class is intended to capture and maintain a reference to an interface, so the reference itself is read-only, but making it writable is a (seemingly) trivial change, if required.
This preserves the interface reference count by maintaining the interface reference but provides a much more simply managed object reference that can be used in all those places that an Integer, Pointer or TObject storage “slot” is provided.
var cust: ICustomer; begin // do something to acquire a cust reference... : // ...and add it to a listbox: lbCustomers.Items.AddObject(cust.Name, TInterface.Create(ICustomer)); end;
One thing to be aware of here is that when subsequently removing or clearing items from a listbox (or clearing Tag, Data or other properties used to hold a TInterface reference) the current contents of course now need to be Free‘d.
Retrieving our stored interface is relatively straightforward:
var intf: TInterface; selectedCust: ICustomer; begin // Get the selected customer: with lbCustomers do intf := TInterface(Items.Objects[ItemIndex]); selectedCust := intf.Ref as ICustomer; end;
The requirement to include the use of the Ref member is more cumbersome than it needs to be however, and beings me to the original question posed in the title of this post. We also need to address the naivety of the implementation (you did spot that in the comment of the original code above, right?).
So first, how to eliminate the need to use the Ref property (indeed, to eliminate the property entirely).
I realised I could do this by implementing my wrapper class itself as an interfaced object but eliminating the reference counted lifetime semantics and indeed delegating ALL interface related behaviour to the captured interface itself.
We can do this in Delphi because the implementation of IUnknown (the basis for all interface behaviour in Delphi) is not hidden in some inaccessible part of the runtime, but is explicit in the implementation in our own code. It only appears automatic and “magic” because we most often inherit this behaviour from a suitable base class.
The only difference here is that the implementation will be provided (and customised) on a class derived directly from TObject with no existing IUnknown support of its own:
TInterface = class(TObject, IUnknown) private fRef: IUnknown; protected // IUnknown function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public constructor Create(const aRef: IUnknown); end; constructor TInterface.Create(const aRef: IUnknown); begin inherited Create; fRef := aRef; end; function TInterface.QueryInterface(const IID: TGUID; out Obj): HResult; begin result := fRef.QueryInterface(IID, Obj); end; function TInterface._AddRef: Integer; begin result := 1; end; function TInterface._Release: Integer; begin result := 1; end;
With this implementation of IUnknown in place, the wrapper class will not have a reference counted lifetime and any interface queries made of it are delegated to the captured interface. When retrieving the captured interface we can now do so directly:
var intf: TInterface; selectedCust: ICustomer; begin // Get the selected customer: with lbCustomers do intf := TInterface(Items.Objects[ItemIndex]); selectedCust := intf as ICustomer; end;
Which leaves only the naivety in the implementation to be address.
The naivety is that the wrapper is holding the reference as an IUnknown but is doing so by using the fact that all interfaces ultimately extend IUnknown and are consequently assignment compatible. The wrapper is not actually obtaining the true IUnknown implementation of the interface.
As it stands, this isn’t actually an issue for the class, as implemented. This naivety only really becomes a problem when comparing interface references for equality:
var a, b: IUnknown; c: ICustomer; begin // Assuming a valid customer reference in c... a := c; b := c as IUnknown; // a <> b !! end;
To be sure of comparing interfaces in this way you have to reduce them to a true, common denominator – e.g. a genuine IUnknown reference.
This requires a simple modification to our constructor and then simplifies the provision of a handy additional function on our wrapper class – an equality test:
constructor TInterface.Create(const aRef: IUnknown); begin inherited Create; fRef := aRef as IUnknown; end; function TInterface.IsEqual(const aOther: IUnknown): Boolean; begin result := ((aOther as IUnknown) = fRef); end;
So, for example, to select a specific customer in a populated listbox:
procedure SelectCustInListbox(const aCustomer: ICustomer; const aListbox: TListbox); var i, selIdx: Integer; begin // Assuming some cust ref to be selected in the list selIdx := -1; for i := 0 to Pred(aListBox.Count) do begin if TInterface(aListBox.Objects[i]).IsEqual(aCustomer) then begin selIdx := i; BREAK; end; end; aListbox.ItemIndex := selIdx; end;
Now of course, it should be readily apparent that such a routine need not be specific to an ICustomer type reference and could be used to select any interface reference in a list box with interface references captured by TInterface objects.
FINAL NOTE: I said that converting the Ref property to a writable value was a “seemingly” trivial change. It is, but if you choose to retain the Ref property in order to make it writable (there is no other purpose for it now that our wrapper exposes the interface more directly) then you should ensure that you use a setter method and convert the interface to IUnknown when updating the captured interface reference into the fRef member. Alternatively you could modify the implementation of IsEqual() to convert both interfaces to IUnknown at the time of comparison