Интерфейсы в Delphi появились не сразу, а когда появилась необходимость поддержать работу с COM и на мой взгляд они не очень стройно вписались в язык.
Скажу честно, я как правило пользуюсь интерфейсами не для взаимодействия с внешним миром посредством механизма СОМ. И, подозреваю, что не только я. В Delphi интерфейсы нашли себе другое полезное применение.
Фактически, интерфейсы полезны в двух случаях:
- Когда необходимо использовать множественное наследование;
- Когда 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