Интерфейсы в 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.