Using Interfaces
When you define an abstract class to represent the base class of a hierarchy, you can come to a point at which the abstract class is so abstract that it only lists a series of virtual functions without providing any implementation. This kind of purely abstract class can also be defined using a specific technique: an interface. For this reason, we refer to these classes as interfaces.Technically, an interface is not a class, although it may resemble one. Interfaces are not classes, because they are considered a totally separate element with distinctive features:
Interface type objects are reference-counted and automatically destroyed when there are no more references to the object. This mechanism is similar to the way Delphi manages long strings and makes memory management almost automatic.
A class can inherit from a single base class, but it can implement multiple interfaces.
Just as all classes descend from TObject, all interfaces descend from IInterface, forming a totally separate hierarchy.
Note | The base interface class was IUnknown until Delphi 5, but Delphi 6 introduced a new name for it—IInterface—to mark even more clearly the fact that this language feature is separate from Microsoft's COM (which uses IUnknown as its base interface). Delphi interfaces are available also in Kylix. |
It is important to note that interfaces support a slightly different OOP model than classes. Interfaces provide a less restricted implementation of polymorphism. Object reference polymorphism is based around a specific branch of a hierarchy. Interface polymorphism works across an entire hierarchy. Certainly, interfaces favor encapsulation and provide a looser connection between classes than inheritance. Notice that the most recent OOP languages, from Java to C#, have the notion of interfaces.Here is the syntax of the declaration of an interface (which, by convention, starts with the letter I):
type
ICanFly = interface
['{EAD9C4B4-E1C5-4CF4-9FA0-3B812C880A21}']
function Fly: string;
end;
This interface has a GUID (Globally Unique Identifier)—a numeric ID following its declaration and based on Windows conventions. You can generate these identifiers by pressing Ctrl+Shift+G in the Delphi editor.
Once you've declared an interface, you can define a class to implement it:
type
TAirplane = class (TInterfacedObject, ICanFly)
function Fly: string;
end;
The RTL already provides a few base classes to implement the basic behavior required by the IInterface interface. For internal objects, use the TInterfacedObject class I've used in this code.You can implement interface methods with static methods (as in the previous code) or with virtual methods. You can override virtual methods in inherited classes by using the override directive. If you don't use virtual methods, you can still provide a new implementation in an inherited class by redeclaring the interface type in the inherited class, rebinding the interface methods to new versions of the static methods. At first sight, using virtual methods to implement interfaces seems to allow for smoother coding in inherited classes, but both approaches are equally powerful and flexible. However, the use of virtual methods affects code size and memory.
Note | The compiler has to generate stub routines to fix up the interface call entry points to the matching method of the implementing class, and adjust the self pointer. The interface method stubs for static methods have to adjust self and jump to the real method in the class. The interface method stubs for virtual methods are much more complicated, requiring about four times more code (20 to 30 bytes) in each stub than the static case. Also, adding more virtual methods to the implementing class just bloats the virtual method table (VMT) that much more in the implementing class and all its descendents. An interface already has its own VMT, and redeclaring an interface in descendents to rebind the interface to new methods in the descendent is just as polymorphic as using virtual methods, but much smaller in code size. |
Now that you have defined an implementation of the interface, you can write some code to use an object of this class, through an interface-type variable:
var
Flyer1: ICanFly;
begin
Flyer1 := TAirplane.Create;
Flyer1.Fly;
end;
As soon as you assign an object to an interface-type variable, Delphi automatically checks to see whether the object implements that interface, using the as operator. You can explicitly express this operation as follows:
Flyer1 := TAirplane.Create as ICanFly;
Whether you use the direct assignment or the as statement, Delphi does one extra thing: It calls the _AddRef method of the object (defined by IInterface). The standard implementation of this method, like the one provided by TInterfacedObject, is to increase the object's reference count. At the same time, as soon as the Flyer1 variable goes out of scope, Delphi calls the _ Release method (again part of IInterface). The TInterfacedObject's implementation of _Release decreases the reference count, checks whether the reference count is zero, and, if necessary, destroys the object. For this reason, the previous example doesn't include any code to free the object you've created.In other words, in Delphi, objects referenced by interface variables are reference-counted, and they are automatically de-allocated when no interface variable refers to them any more.
Warning | When using interface-based objects, you should generally access them only with object references or only with interface references. Mixing the two approaches breaks the reference counting scheme provided by Delphi and can cause memory errors that are extremely difficult to track. In practice, if you've decided to use interfaces, you should probably use exclusively interface-based variables. If you want to be able to mix them, instead, disable the reference counting by writing your own base class instead of using TInterfacedObject. |