Интерфейсы в Delphi появились, когда понадобилось поддержать работу с COM и они не очень стройно вписались в язык. В итоге смешивать работу с классами и интерфейсами следует крайне осторожно, всему виной счетчик ссылок, значение которого в классах изначально равно нулю.
В качестве примера форма с одной кнопкой.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMyClass = class(TInterfacedObject)
public
destructor Destroy; override;
end;
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure Kill(Intf: IInterface);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TMyClass }
destructor TMyClass.Destroy;
begin
ShowMessage('TMyClass.Destroy');
inherited;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
try
Kill(MyClass);
finally
MyClass.Free;
end;
end;
{$O-} // выключим оптимизатор, чтобы он не выбросил обращение к Intf
procedure TForm1.Kill(Intf: IInterface);
begin
// Используем интерфейс
// ...
ShowMessage('TMyClass.Kill');
end;
{$O+}
end.
При нажатии на кнопку появляются сообщения:
- TMyClass.Kill
- TMyClass.Destroy
- TMyClass.Destroy
- Access violation at address 00403BD2 in module ‘Project1.exe’. Read of address FFFFFFF8.
Почему такое происходит?
Разберем ход выполнения процедуры Kill:
// Изначально Intf.RefCount = 0, это нормальное состояние для TInterfacedObject
// Выполняется Intf._AddRef, теперь RefCount = 1
procedure TForm1.Kill(Intf: IInterface);
begin
ShowMessage('TMyClass.Kill');
// Интерфейс выходит из области видимости, выполняется Intf._Release
// И, так как RefCount стал равень нулю, объект уничтожается: TMyClass.Destroy
// Это и становится причиной того, что дальше все идет наперекосяк.
end;
Способ обойти такую проблему есть — переопределить методы _AddRef и _Release таким образом, чтобы обнуление счетчика ссылок не вызывало освобождение объекта. Но такой шаг увеличивает сложность, т.к. в коде, где часть интерфейсов использует счетчик ссылок, а часть нет, легко запутаться. Тем не менее, в VCL переопределение счетчика ссылок используется. У наследников TComponent счетчик ссылок то есть, то его нет :)
function TComponent._AddRef: Integer;
begin
if FVCLComObject = nil then
Result := -1 // -1 indicates no reference counting is taking place
else
Result := IVCLComObject(FVCLComObject)._AddRef;
end;
function TComponent._Release: Integer;
begin
if FVCLComObject = nil then
Result := -1 // -1 indicates no reference counting is taking place
else
Result := IVCLComObject(FVCLComObject)._Release;
end;
Врядли в языке без сборщика мусора можно было бы реализовать интерфейсы более удобно. Разве что принудить программиста явно вызывать _AddRef и _Release.
Update
Аналогичная проблема и попытка избежать уничтожения объекта при использовании функции Supports:
http://delphisorcery.blogspot.com/2011/10/supports-killing-objects.html
Update 2
Расширенная версия статьи на Хабре.