Interfaces in Delphi did not appear immediately, but when it became necessary to support work with COM and in my opinion they did not fit very well in the language.
Frankly, I usually use interfaces not to interact with the outside world through the COM mechanism. And I suspect that not only me. In Delphi, interfaces have found another useful application.
In fact, interfaces are useful in two cases:
- When it is necessary to use multiple inheritance;
- When ARC (automatic reference counting) seriously facilitates memory management.
In Delphi, historically, there is no and there was no multiple inheritance in the form that is common in some other programming languages ​​(for example, C ++). And this is good.
')
In Delphi, multiple inheritance problems are solved by interfaces. The interface is a completely abstract class, all methods of which are virtual and abstract. ( GunSmoker )
And this is almost true, but not quite so! Interfaces are very similar to abstract classes. Very similar, but in the end, classes and interfaces behave very differently.
In connection with the upcoming changes, that is, as the ARC appears in the new compiler, the topic of managing the life of Delphi objects gets new relevance, since the new “sacred wars” are predictable. I would not like to stand up sharply on one side or the other right now, I just want to explore the existing areas of intersection of the “classical” approach and the “reference” mechanisms for managing the life of an object as a programmer-practitioner.
Nevertheless, let me express the hope that the ARC in the new compiler will make it possible to really perceive the interfaces of everything — only as abstract classes. Although I treat such revolutionary changes with caution.
Often, the programmers of the “interface snouts” to the database ignore the issues of managing the memory of objects, which does not detract from the importance of the topic. In my opinion, mixing classes and interfaces should be extremely careful. All the fault of the reference counter. To understand this, let's do a simple exercise.
As an example, a form with one button. Especially test example. Do not repeat this at home.
unit Unit1; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type IMyIntf = interface procedure TestMessage; end; TMyClass = class(TInterfacedObject, IMyIntf) public procedure TestMessage; destructor Destroy; override; end; TForm1 = class(TForm) Button1: TButton; Memo1: TMemo; procedure Button1Click(Sender: TObject); public procedure Kill(Intf: IMyIntf); end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); var MyClass: TMyClass; begin Memo1.Clear; try MyClass := TMyClass.Create; try Kill(MyClass); finally MyClass.Free; end; except on E: Exception do Memo1.Lines.Add(E.Message); end; end; procedure TForm1.Kill(Intf: IMyIntf); begin Intf.TestMessage; end; destructor TMyClass.Destroy; begin Form1.Memo1.Lines.Add('TMyClass.Destroy'); inherited; end; procedure TMyClass.TestMessage; begin Form1.Memo1.Lines.Add('TMyClass.TestMessage'); end; end.
Run, press the button and the following text appears in Memo1:
TMyClass.TestMessage
TMyClass.Destroy
TMyClass.Destroy
Invalid pointer operation
Destroy is called twice and as a result - “Invalid pointer operation”. Why?
Once is clear. In the Button1Click handler, MyClass.Free is called. And where is the second time from? The essence of the problem lies in the procedure Kill. Let us analyze the progress of its implementation.
That is, the problem is that with TInterfacedObject and its heirs, the value of the reference count is zero. This is normal for an object, but for an interface it is a sign of imminent and inevitable death.
Who is to blame and what to do?
I think no one is to blame. No need to do so. Hardly in a language without a garbage collector, it would be possible to implement interfaces with a controlled lifetime more conveniently. Unless to force the programmer to explicitly call _AddRef and _Release. I doubt that would be more convenient.
It was also possible to introduce two types of interfaces - with and without a reference counter, but this would add more confusion.
It should be understood that the reference count does not belong to the interfaces, but to the object. Interfaces are only controlled by this counter. If there are two types of interfaces in Delphi, how should an object that implements two interfaces of different types behave in such a situation? There is plenty of room for finding potential pitfalls.
You can get rid of the object reference count yourself by redefining the _AddRef and _Release methods so that zeroing the reference count does not cause the object to be released. For example, changing the class from the example in the following way (for the class to inherit the interface, it must implement three methods: _AddRef, _Release and QueryInterface):
TMyClass = class(TObject, IMyIntf) protected function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; public procedure TestMessage; destructor Destroy; override; end; function TMyClass.QueryInterface(const IID: TGUID; out Obj): HResult; begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; function TMyClass._AddRef: Integer; begin Result := -1; end; function TMyClass._Release: Integer; begin Result := -1; end;
But such a step increases the complexity, since in the code where the same interface implemented in different objects uses the reference counter, it’s not easy to get confused.
However, in the VCL, the reference count override is used. For the successors of TComponent, the link counter works quite intricately.
function TComponent._AddRef: Integer; begin if FVCLComObject = nil then Result := -1
You can approach the situation from the other side and slightly change the Kill procedure by adding const to the parameter definition. In this case, everything will start to work as it should, since the reference counter will simply not be involved:
procedure TForm1.Kill(const Intf: IMyIntf); begin Intf.TestMessage; end;
Now the result will be that, that is, absolutely expected:
TMyClass.TestMessage
TMyClass.Destroy
Both the first and second ways to get around the problem only increase the number of nuances that should be kept in mind when working with the code and thus potentially increase the number of errors in it. So I would recommend mixing work with interfaces and objects only when there really is a need. And by all means avoid in all other cases.
And if earlier, when working with VCL, many could never really encounter the need to use interfaces, in the light of the new FireMonkey library, which seems to be cross-platform-like, you need to carefully monitor the use of interfaces within itself, without relying on the “ideological the slimness of the language possibilities offered by Embarcadero.