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

в 10:28, , рубрики: 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.

Автор: RomanYankovsky

Источник

Поделиться

* - обязательные к заполнению поля