Интерфейсы в 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
Расширенная версия статьи на Хабре.
1. Алексей
16 Июл 2012 6:58 пп
Безусловно интерфейсы не совсем вписываются в общую концепцию Delphi и работа с ними, особенно для новичка, очень, очень сложна. Но, тем не менее, они нужны, т.к. с Office’ом через COM работать [относительно] удобно.
2. Neo][
17 Июл 2012 6:29 дп
Ни разу не видел, чтобы с интерфейсами работали подобным образом :)
Если работать «именно» с интерфейсом, тогда счётчик ссылок будет работать в большинстве случаев правильно.
3. Роман Янковский
17 Июл 2012 7:05 дп
Neo][, мир несовершенен. Иногда это единственный способ дать единое поведение нескольким уже существующим классам. Есть еще для этого вариант с TObject.Dispatch, но по-моему он еще менее красив.
4. Дмитрий
19 Июл 2012 3:18 пп
Хм. А при включенном оптимизаторе все нормально работает. Что и верно т.к.интерфейс удален. Ладно идем далее, объявляем Kill как
{$O-} // выключим оптимизатор, чтобы он не выбросил обращение к Intf
procedure TForm1.Kill(const Intf: IInterface);
begin
ShowMessage(‘TMyClass.Kill’);
end;
{$O+}
тоже ошибки нет, т.к. const дает неявное превращение счетчика и поэтому объект не удаляется 2 раза. Что правильнее…
5. Роман Янковский
19 Июл 2012 3:52 пп
С const интересно вышло, об этом я даже как-то не думал.
6. Alex W. Lulin
31 Мар 2013 10:16 пп
http://18delphi.blogspot.com/2013/03/blog-post_7527.html
7. Alex W. Lulin
31 Мар 2013 10:19 пп
ВЫ ПРАВЫ — проблема со счётчиком ссылок. Завтра — допишу про свою реализацию. Там по крайней мере — всё симметрично.
Ну и const — ДА — если const, то счётчик ссылок НЕ ТРОГАЕТСЯ. Если БЕЗ — const — счётчик ссылок увеличивается при входе в процедуру и уменьшается при выходе.
А ещё если with MyInterface do begin .. end; написать — та же история со счётчиком ссылок как и при отсутствии const.