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;
or
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]);
or
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
A little clarification – the code you use to compare interface references assumes that the interfaces compared are implemented by the same object. Generally it is not so, though if the interfaces are implemented by different objects comparison makes no sense at all.
@Serg: Yes, that is true. I can’t recall ever having come across such a scenario however. And I admit, my TInterface was devised to address this need in an application where the interfaces involved were not COM-based and the relationships between the interfaces and the implementing classes was otherwise very straightforward. The IsEquals() routine was more or less an afterthought for convenience – the wrapper class and interface delegation were the key functions served by the class.
Yes, I probably was a bit messy in my previous comment. By comparing interface references the way you do it you actually compare the references to objects implementing the interfaces. Usually it make sense for internal application’s interfaces, but the general idea of interface is to hide the implementation details.
@Serg: No, it’s not comparing the implementing objects, it’s comparing the IUnknown implementation of two disparate interfaces. If interfaces A and B both yield the same IUnknown when explicitly queried for it then they are – for the purposes of this implementation – the same. That is not to say that may might not be semantically treated as the same on some other basis. It is to avoid the careless and easily made mistake of holding an IUnknown reference without actually obtaining IUnknown and then comparing directly with some actual IUnknown obtained from that interface and incorrectly determining that they are NOT equal when in fact they are.
I guess you didn’t try your example code. Without an explicit cast this won’t work.
var
selectedCust: TCustomer;
begin
// Get the selected customer:
selectedCust := lbCustomers.Items.Objects[lbCustomers.ItemIndex];
end;
@Olaf: Yes, quite right, thought the “example” code was tried, just not copy/pasted from the working exemplar. 🙂 Well spotted. (and now fixed)
You might want to rearrange the order of your survey.
I couldn’t decide between “good thing” and “bad thing” and there was nothing in between to indicate a neutral opinion.
Only after voting did I see there is an “irrelevant” right down at the bottom!
You mention the “WITH” statement. It is indeed poorly implemented in Delphi.
But folks with experience in Visual Basic (which also has such a construct) will tell you it can be is implemented in a better way. I recall there were several serious and well-thought-out concepts submitted to the old Borland wishlist which would turn WITH into a useful tool.
In an e-mail exchange with Nick Hodges (peace be upon his name) it became clear it would require changes to the debugging system, so I expect it will never get done.
What I cannot understand is that in Turbo Pascal, the debugger was at least capable of showing a message “Cannot display this variable”, or words to that effect. So the old debugger knew it was looking at a WITH-resolved value!
In Delphi, it either shows nothing at all, or, most confusingly, if it finds a variable of tee same name, it shows the value of that!!!
In Delphi, it either shows nothing at all, or, most confusingly, if it finds a variable of the same name, it shows the value of that!!!
Now I see what I missed – your trick with assignment to IUnknown without interface casting. Interesting, never thought about it.
Hello, I post just to mention that your TInterface class seems close to TAggregatedObject of system.pas.
Does using IUnknown instead of a pointer makes a difference ?
@Damien: There is a key difference between TInterface and TAggregatedObject: TAggregatedObject specifically does not maintain it’s own reference count (+1) on the referenced interface.
If the same approach were used for TInterface then at the point at which you went to retrieve the interface reference from the object it might be referencing an object which had already been destroyed (because all other strong references had been released in the meantime).
This isn’t typically the case with TAggregatedObject since it is intended to be used when the derived class instance lifetime is explicitly controlled by the interfaced object it references, i.e. the aggregated object can itself only exist if, when and whilst it’s controller exists.
TInterface is intended to be used quite differently, i.e simply as a TObject “compatible” strong reference to an interface, i.e. keeping the referenced object “alive” for as long as the TInterface reference exists.
@Ken: What *I* cannot understand is how/why the IDE and compiler teams can make this work whilst the debugger team cannot. 🙂
I don’t want to turn the comments for this post into (yet) another discussion about “with” though, so let’s leave it there for now, eh? 🙂
Ha! Was a bit of a hijack on my part, now that I think of it. Sorry!!! 🙂
Since you’re just delegating the interface to the contained object, wouldn’t this be less verbose?
TInterface = class(TObject,IUnknown)
private
FRef : IUnknown;
property Ref : IUnknown read FRef implements IUnknown;
public
constructor Create(ARef : IUnknown);
end;
Todd, yes you are quite right. 🙂
At the time that I implemented this I remember being not entirely sure of what would occur w.r.t reference counting and felt that I could avoid that uncertainty by being explicit in my implementation of IUnknown on TInterface itself.
It also means I don’t have to remember the finer points and implications of implements when looking at the TInterface class in the future (though in this case it would have been easy enough to add some documentation explaining how and why “implements” worked).
I had to contrive some test code to explore the behaviour of implements in this area and do some research to be sure my results tallied with the expected and described behaviour. It’s ironic that I avoided that extra work when implementing the class but then incurred it anyway in order to respond to your comment. 🙂
Engineers love irony!