Особенности применения интерфейсов в Delphi

от автора

Интерфейсы в Delphi появились не сразу, а когда появилась необходимость поддержать работу с COM и на мой взгляд они не очень стройно вписались в язык.

Скажу честно, я как правило пользуюсь интерфейсами не для взаимодействия с внешним миром посредством механизма СОМ. И, подозреваю, что не только я. В Delphi интерфейсы нашли себе другое полезное применение.

Фактически, интерфейсы полезны в двух случаях:

  1. Когда необходимо использовать множественное наследование;
  2. Когда ARC (автоматический подсчет ссылок) серьезно облегчает управление памятью.

В Delphi исторически нет и не было множественного наследования в той форме, как это принято в некоторых других языках программирования (например, С++). И это хорошо.

В Delphi проблемы множественного наследования решаются интерфейсами. Интерфейс — это полностью абстрактный класс, все методы которого виртуальны и абстрактны. (GunSmoker)

И это практически так, но не совсем так! Интерфейсы очень похожи на абстрактные классы. Очень похожи, но в конечном итоге классы и интерфейсы ведут себя очень по-разному.

В связи с грядущими изменениями, то есть по мере появления ARC в новом компиляторе тема управления жизнью Delphi-объектов получает новую актуальность, так как прогнозируемо будут новые «священные войны». Мне бы не хотелось именно сейчас резко вставать на ту или иную сторону, хочется лишь поисследовать существующие области пересечения «классического» подхода и «ссылочных» механизмов управления жизнью объекта как программисту-практику.

Тем не менее, позволю себе выразить надежду на то, что ARC в новом компиляторе даст возможность действительно воспринимать интерфейсы всего-лишь как абстрактные классы. Хотя я отношусь к подобным революционным изменениям с опаской.

Часто программисты «интерфейсных морд» к БД игнорируют вопросы управления памяти объектов, что не умаляет важность темы, которая до сих пор возбуждает явный интерес профессионалов, которые продолжают исследовать применимость «интерфейсов» для выработки альтернативных классическим подходов.

По моему мнению, смешивать в работе классы и интерфейсы следует крайне осторожно. Всему виной счетчик ссылок. Для понимания этого давайте проделаем простое упражнение.

В качестве примера – форма с одной кнопкой. Сугубо тестовый пример. Не повторяйте это дома.

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;  { TMyClass }  destructor TMyClass.Destroy; begin   Form1.Memo1.Lines.Add('TMyClass.Destroy');   inherited; end;  procedure TMyClass.TestMessage; begin   Form1.Memo1.Lines.Add('TMyClass.TestMessage'); end;  end. 

Запускаем, нажимаем кнопку и в Memo1 появляется следующий текст:

TMyClass.TestMessage TMyClass.Destroy TMyClass.Destroy Invalid pointer operation

Destroy вызывается два раза и как результат – «Invalid pointer operation». Почему?

Один раз – это понятно. В обработчике Button1Click вызывается MyClass.Free. А второй раз откуда? Суть проблемы кроется в процедуре Kill. Разберем ход ее выполнения.

// Изначально Intf.RefCount = 0, это нормальное состояние для TInterfacedObject // Интерфейс Intf заходит в область видимости процедуры Kill // Выполняется Intf._AddRef, теперь RefCount = 1 procedure TForm1.Kill(Intf: IMyIntf); begin   Intf.TestMessage;    // Интерфейс выходит из области видимости, выполняется Intf._Release   // И, так как RefCount стал равень нулю, объект уничтожается: TMyClass.Destroy   // Это и становится причиной того, что дальше все идет не так, как ожидалось.   // Дальнейшая работа с этим классом невозможна. end; 

То есть проблема в том, что у TInterfacedObject и его наследников значение счетчика ссылок равно нулю. Для объекта это нормально, но для интерфейса это признак скорой и неминуемой смерти.

Кто виноват и что делать?

Думаю, никто не виноват. Врядли в языке без сборщика мусора можно было бы реализовать интерфейсы с управляемым временем жизни более удобно. Разве что принудить программиста явно вызывать _AddRef и _Release. Сомневаюсь, что это было бы удобнее.
Так же можно было ввести два типа интерфейсов – со счетчиком ссылок и без, но это внесло бы еще больше путаницы.

Следует понимать, что счетчик ссылок принадлежит не интерфейсам, а объекту. Интерфейсы этим счетчиком лишь управляют. Если в Delphi будет два типа интерфейсов, то как в такой ситуации вести себя объекту, который реализует два интерфейса разных типов? Здесь большой простор для поиска потенциальных подводных камней.

От счетчика ссылок объекта можно избавиться самостоятельно переопределив методы _AddRef и _Release таким образом, чтобы обнуление счетчика ссылок не вызывало освобождение объекта. Например, изменив класс из примера таким образом (чтобы класс мог наследовать интерфейс он должен реализовать три метода: _AddRef, _Release и 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;  

Но такой шаг увеличивает сложность, так как в коде, где один и тот же интерфейс реализованный в разных объектах то использует счетчик ссылок, то нет, легко запутаться.

Тем не менее, в 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; 

Можно подойти к ситуации с другой стороны и немного изменить процедуру Kill, добавив const в определение параметра. В этом случае все начнет работать как следует, так как счетчик ссылок просто не будет задействован:

procedure TForm1.Kill(const Intf: IMyIntf); begin   Intf.TestMessage; end; 

Теперь результат будет таким, то есть абсолютно ожидаемым:

TMyClass.TestMessage TMyClass.Destroy

И первый и второй способ обойти проблему лишь увеличивают количество нюансов, которые следует удерживать во внимании при работе с кодом и тем самым потенциально увеличивают количество ошибок в нем. Так что я бы рекомендовал смешивать работу с интерфейсами и объектами только тогда, когда эта необходимость действительно есть. И всеми силами избегать во всех остальных случаях.

И если раньше при работе с VCL многие могли вообще никогда не сталкиваться по-настоящему с необходимостью использовать интерфейсы, то в свете новой библиотеки FireMonkey, дающей вроде-как кроссплатформенность, нужно очень внимательно следить за использованием интерфейсов внутри неё самой, не полагаясь на «идеологическую стройность» языковых возможностей, предлагаемых Embarcadero.

ссылка на оригинал статьи http://habrahabr.ru/post/181107/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *