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
Tags: Delphi, interface, interfaces, IUnknown
-
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.
-
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!!!
-
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 ? -
Ha! Was a bit of a hijack on my part, now that I think of it. Sorry!!!



DelphiFeeds
14 comments
Comments feed for this article
Trackback link: http://www.deltics.co.nz/blog/wp-trackback.php?p=644