Delphi's interfaces are rooted in COM interop, but in most cases are used for non COM purposes. Unfortunately, there are some big limitations with Delphi interfaces, and in a critical area where interfaces are the most useful.
Exception, Not the Rule
I rarely use interfaces. I find that in many cases, they add complexity, rather than solve it. However, there are valid cases for interfaces such as IEnumerable
, data binding, IList
, etc. Interfaces allow much of what is often sought after with multiple inheritance, but without multiple inheritance's problems.
Object Interface Support
The goal of using an interface is to allow a common API among objects that otherwise do not share a common ancestor other than TObject
. Interfaces can also be useful in allowing limited exposure of private
members.
In Delphi however, not all objects can be used with interfaces. To use an object with interfaces, one must add special code for reference counting, or descend from (directly or indirectly) one of the specialized classes that already supports interfaces. These classes are:
TInterfacedObject
TInterfacedPersistent
TComponent
There may be others as well, but these are the primary players. Likely it was done this way due to concerns of adding additional baggage to the entire object tree. Unfortunately, this causes some problems as well.
They are declared as follows:
TObject
TInterfacedObject
TPersistent
TInterfacedPersistent
TComponent
If all of the objects that you wish to use a specified interface on all descend from the same class that adds interface support, all is well. However if you have objects that inherit their interface support from different classes, then there is a big problem. It does not work.
For example, imagine we have an interface ILife
and we have 2 classes. One class inherits from TComponent
, another from TInterfacedPersistent
. Each can implement ILife
, but we cannot simply get an ILife
interface using the common ancestor TPersistent
.
Using TComponent
First, let me show you a simple example where three classes all descend from TComponent
.
unit UnitA;
interface
uses
System.SysUtils, System.Classes;
type
ILife = interface
['{BDC73295-9F45-4BEC-B726-77DEC9B9EAAC}']
function GetAnswer: Integer;
end;
TComponentA = class(TComponent, ILife)
function GetAnswer: Integer;
end;
TComponentB = class(TComponent, ILife)
function GetAnswer: Integer;
end;
TComponentC = class(TComponentB, ILife)
end;
procedure TestA;
implementation
procedure TestIntf(aComp: TComponent);
var
i: integer;
xILife: ILife;
begin
xILife := aComp as ILife;
i := xILife.GetAnswer;
WriteLn('Answer: ' + i.ToString);
end;
procedure TestA;
var
xCompA, xCompB, xCompC: TComponent;
begin
WriteLn('TestA');
xCompA := TComponentA.Create(nil); try
TestIntf(xCompA);
finally xCompA.Free; end;
xCompB := TComponentB.Create(nil); try
TestIntf(xCompB);
finally xCompB.Free; end;
xCompC := TComponentC.Create(nil); try
TestIntf(xCompC);
finally xCompC.Free; end;
WriteLn;
end;
function TComponentA.GetAnswer: Integer;
begin
Result := 42;
end;
function TComponentB.GetAnswer: Integer;
begin
Result := 22;
end;
end.
This code works fine and produces the expected output:
TestA
Answer: 42
Answer: 22
Answer: 22
The Problem
The problem is that this only works if all of the classes which use ILifeA
all descend from TComponent
. This limitation largely defeats one of the primary purposes of interfaces. Let's change one of the classes to descend from TInterfacedObject
instead. TInterfacedObject
is a direct descendant of TObject
, and exists only to add interface support.
For example, this will not work:
type
ILife = interface
['{CED2DFC6-4E8E-42F2-A724-2E3D8539192F}']
function GetAnswer: Integer;
end;
TObjectB1 = class(TObject, ILifeB)
function GetAnswer: Integer;
end;
If you try to compile this, the following error will occur:
[dcc32 Error] UnitB.pas(14): E2291 Missing implementation of interface method IInterface.QueryInterface
[dcc32 Error] UnitB.pas(14): E2291 Missing implementation of interface method IInterface._AddRef
[dcc32 Error] UnitB.pas(14): E2291 Missing implementation of interface method IInterface._Release
[dcc32 Error] UnitB.pas(27): E2015 Operator not applicable to this operand type
[dcc32 Error] UnitB.pas(38): E2034 Too many actual parameters
[dcc32 Fatal Error] Project1.dpr(9): F2063 Could not compile used unit 'UnitB.pas'
This is because TObject
does not have the necessary scaffolding required for interfaces. We can solve this easily by changing the ancestor to TInterfacedObject
instead of TObject
:
type
ILife = interface
['{26621188-5BE3-42B4-A906-2963B480C1F4}']
function GetAnswer: Integer;
end;
TObjectC1 = class(TInterfacedObject, ILife)
private
function GetAnswer: Integer;
public
function GetFoo: integer;
end;
All should be good now right? Well, no. There are MORE problems. Look at this code:
procedure TestIntf(aObj: TInterfacedObject);
var
i: integer;
xILife: ILifeC;
begin
xILife := aObj as ILife;
i := xILife.GetAnswer;
WriteLn('Answer: ' + i.ToString);
end;
procedure TestC_1;
var
xObjC1: TInterfacedObject;
begin
WriteLn('TestC_1');
xObjC1 := TObjectC1.Create; try
TestIntf(xObjC1);
finally xObjC1.Free; end;
WriteLn;
end;
On quick look, one would expect this should work fine. However, it does not because TInterfacedObject
changes how objects work when interfaces are actually used, but not when they are not. This code will crash on this statement:
xObjC1.Free;
Why? Because when we use the ILife
interface and we are done with it, the compiler uses reference counting and frees the whole object. You can see this in action by adding a dummy destructor to TObjectC1
. Then set a breakpoint and look at the call stack. The destructor will be called after TestIntf
is called.
New Problem
Interfaces when used with classes that descend from TComponent
do not exhbit this behavior that for Delphi is non standard in non ARC compilers (Windows). So now objects act differently when used with interfaces depending on their ancestor.
So now we have to just think about sometimes destroy sometimes not? Well, it is not that simple either. Now we don't have to free the object, except sometimes! If no interface is used, then we must NOT free it. If an interface is not used, we MUST free it. The problem is, as the object is passed around to other methods, how are we to know if any code takes an interface for it or not?
If an interface is used, anywhere down the line...
xObjC1 := TObjectC1.Create;
TestIntf(xObjC1);
Do we free it or not?
xObjC1 := TObjectC1.Create; try
i := xObjC1.GetFoo;
finally xObjC1.Free; end;
New hard to find bugs:
xObjC1 := TObjectC1.Create;
TestIntf(xObjC1);
i := xObjC1.GetFoo;
When your code logic becomes deeper and you add multiple interfaces to a class, the problem gets even worse.
Some of this can be addressed using unsafe and/or weak directives. However, this isn't simply declared on the declaration, and must be used in user code references. A bad solution in my opinion as well.
The Solution?
The supposed solution is to use interface references instead of object references everywhere for such objects. But this defeats much of the reason that interfaces are used and when multiple interfaces on an object are used, the problem only gets worse.
The Tree Problem
A primary use of interfaces is to expose a common interface from disparate objects. Yet, in many cases, this is not workable in Delphi.
type
ILife = interface
['{7C8E0C18-F8A5-43DF-8999-BF17D6EC961C}']
function GetAnswer: Integer;
end;
TComponentA = class(TComponent, ILife)
function GetAnswer: Integer;
end;
TObjectA = class(TInterfacedObject, ILife)
function GetAnswer: Integer;
end;
TPersistentA = class(TInterfacedPersistent, ILife)
function GetAnswer: Integer;
end;
Unfortunately, these are largely unusable to obtain an interface in a generic way. This will not compile:
procedure TestIntf(aObj: TObject);
var
i: integer;
xILife: ILife;
begin
xILife := aObj as ILife;
i := xILife.GetAnswer;
WriteLn('Answer: ' + i.ToString);
end;
Now the obvious solution is to pass the interface instead. However, this is not always practical and again eliminates one of the major benefits of interfaces. There are some work arounds, but then we have the problem that if a class descends from:
TComponent
- we MUST free it or use Owner to free it. TInterfacedPersistent
, we MUST free it. TInterfacedObject
- If any code anywhere used an interface, we must NOT free it.
- If no code used an interface, we MUST free it.
Seriously? Someone thought this is a good idea?
TInterfacedObject Misnomer
If TInterfacedObject
really must work this way, it should have been called TARCObject
or something distinctive. TInterfacedPersistent
is TPersistent
with interface support, yet TInterfacedObject
is an TObject
with interface support AND non standard lifecycle management? TInterfacedObject documentation references Memory Management of Interface Objects, but this topic only has two short paragraphs and barely a hint at the problems it introduces.
But Just Use Interfaces!
Yeah. I get it. As discussed prior, there are "workarounds" to using TInterfacedObject
by using only interface references. But if you are still thinking this is a "solution", you have totally missed the point.
Using only interface references causes complexity in accessing non interface members. There is also a lack of documentation on this issue. And then, we have the life cycle issues should the executed code paths not use an interface, and multiple interfaces require extra code as well.
A Persistent Problem
TInterfacedPersistent
does not have the free/maybe free problem that TInterfacedObject
does.
TComponent
inherits from TPersistent
, but not TInterfacedPersistent
.
TComponent = class(TPersistent, IInterface, IInterfaceComponentReference)
The VCL declares them like this:
TPersistent
TInterfacedPersistent
TComponent
This means that if we have:
type
ILife = interface
['{26621188-5BE3-42B4-A906-2963B480C1F4}']
function GetAnswer: Integer;
end;
TObjectC1 = class(TInterfacedObject, ILife)
private
function GetAnswer: Integer;
public
function GetFoo: integer;
end;
TPersistentC1 = class(TInterfacedPersistent, ILife)
private
function GetAnswer: Integer;
public
destructor Destroy; override;
end;
We still cannot use the interface from a TPersistent
reference, even though both of them have TPersistent
as a base class. If they had declared them this way:
At least we could have used interfaces properly between TComponent
and TInterfacedPersistent
. The interface scaffolding is slightly different between the two though and prevents this. Interfaces and TComponent
have issues as well. Although TInterfacedPersistent
and TComponent
have different interface scaffolding, TComponent
could still have been changed to inherit from TInterfacedPersistent
, and the interface scaffolding (implemented as methods) could have been overridden. This would have provided an easy solution to this problem.
You can also create your own interface scaffolding on your objects or a base to use, however it doesn't solve the tree problem.
Hacking a Solution
There is a hacky solution - one that should not be needed. This could also be done using RTTI (Runtime reflection for non Delphi developers reading this), but not exactly optimal either. To get an interface crossing different interface scaffolding entry points in the tree, one can do:
function GetALife(aObject: TPersistent): ILife;
begin
if aObject is TInterfacedPersistent then begin
Result := TInterfacedPersistent(aObject) as ILife;
end else if aObject is TComponent then begin
Result := TComponent(aObject) as ILife;
end else begin
raise Exception.Create('Cannot obtain interface.');
end;
end;
This provides a reasonably usable solution, although it should not be necessary in the first place. This method requires a function to be added for each interface. I have avoided implementing TInterfacedObject
as I will be avoiding it like the plague, but it can be made to work as well but will of course introduce its lifecycle management problems which will differ from when ILife
is returned from TInterfacedPersistent
and TComponent
.
Conclusion
As if I didn't have enough reasons to avoid interfaces, the way Delphi implements them only adds to the baggage and has made interfaces almost completely useless for me as they add far more code and risk than they help. The net gain for me is strongly negative except in rare cases.
Without adding heavy baggage to TObject
, it would have been better to build support into it to allow getting a single interface even if there are separate scaffolding implementations. An interface should be an interface, not dependent on specifics of the class itself to determine compatibility as well as lifecycle management. Further more, the life management of TInterfacedObjects
as it is implemented makes them a very dangerous class to use and "proper use" of them severely limits their usefulness.
Unless you absolutely need TInterfacedObject
, I suggest to completely avoid it and use TInterfacedPersistent
instead. TInterfacedPersistent
adds RTTI support. If you do not wish to have that overhead, you can make your own TInterfacedObjectThatIsntDrunk
by cloning TInterfacedPersistent
but changing it to inherit from TObject
instead of TPersistent
, and making minor adjustments.