Счетчик ссылок интерфейса Delphi

Сегодня я столкнулся с какой-то странной ситуацией.

У меня есть несколько интерфейсов и объектов. Код выглядит следующим образом:

IInterfaceZ = interface(IInterface)
['{DA003999-ADA2-47ED-A1E0-2572A00B6D75}']
  procedure DoSomething;
end;

IInterfaceY = interface(IInterface)
  ['{55BF8A92-FCE4-447D-B58B-26CD9B344EA7}']
  procedure DoNothing;
end;

TObjectB = class(TInterfacedObject, IInterfaceZ)
  procedure DoSomething;
end;

TObjectC = class(TInterfacedObject, IInterfaceY)
public
  FTest: string;
  procedure DoNothing;
end;

TObjectA = class(TInterfacedObject, IInterfaceZ, IInterfaceY)
private
  FInterfaceB: IInterfaceZ;
  FObjectC: TObjectC;
  function GetBB: IInterfaceZ;
public
  procedure AfterConstruction; override;
  procedure BeforeDestruction; override;
  property BB: IInterfaceZ read GetBB implements IInterfaceZ;
  property CC: TObjectC read FObjectC implements IInterfaceY;
end;

procedure TObjectB.DoSomething;
begin
  Sleep(1000);
end;

procedure TObjectA.AfterConstruction;
begin
  inherited;
  FInterfaceB := TObjectB.Create;
  FObjectC := TObjectC.Create;
  FObjectC.FTest := 'Testing';
end;

procedure TObjectA.BeforeDestruction;
begin
  FreeAndNil(FObjectC);
  FInterfaceB := nil;
  inherited;
end;

function TObjectA.GetBB: IInterfaceZ;
begin
  Result := FInterfaceB;
end;

procedure TObjectC.DoNothing;
begin
  ShowMessage(FTest);
end;

Теперь, если я получаю доступ к таким различным реализациям, я получаю следующие результаты:

procedure TestInterfaces;
var
  AA: TObjectA;
  YY: IInterfaceY;
  ZZ: IInterfaceZ;
  NewYY: IInterfaceY;
begin
  AA := TObjectA.Create;
  // Make sure that the Supports doesn't kill the object. 
  // This line of code is necessary in XE2 but not in XE4
  AA._AddRef;

  // This will add one to the refcount for AA despite the fact
  // that AA has delegated the implementation of IInterfaceY to
  // to FObjectC.
  Supports(AA, IInterfaceY, YY);
  YY.DoNothing;

  // This will add one to the refcount for FInterfaceB.
  // This is also allowing a supports from a delegated interface
  // to another delegated interface.
  Supports(YY, IInterfaceZ, ZZ);
  ZZ.DoSomething;

  // This will fail because the underlying object is actually
  // the object referenced by FInterfaceB.
  Supports(ZZ, IInterfaceY, NewYY);
  NewYY.DoNothing;
end;

Первый вызов Supports, который использует переменную в инструментах, возвращает YY, который на самом деле является ссылкой на TObjectA. Моя переменная АА считается подсчитанной. Поскольку базовый объект подсчитанных ссылок является TObjectA, вторая поддержка, которая использует интерфейс в вызове поддержки, работает и возвращает мне интерфейс. Основной объект фактически является TObjectB. Внутренний объект за FInterfaceB является объектом, подсчитанным ссылкой. Эта часть имеет смысл, потому что GetBB - это фактически FInterfaceB. Как и ожидалось здесь, последний вызов Supports возвращает null для NewYY, и вызов в конце не работает.

Мой вопрос в том, ссылается ли ссылка на TObjectA на первый вызов поддержки по дизайну? Другими словами, когда свойство, реализующее интерфейс, возвращает объект, а не интерфейс, означает ли это, что объектом владельца будет тот, который делает подсчет ссылок? Мне всегда казалось, что реализация также приведет к тому, что внутренний делегированный объект будет считаться ссылкой вместо основного объекта.

Объявления следующие:

  property BB: IInterfaceZ read GetBB implements IInterfaceZ;

С помощью этой опции выше внутренний объект за FInterfaceB - это тот, который подсчитывается ссылкой.

  property CC: TObjectC read FObjectC implements IInterfaceY;

С помощью этого второго варианта выше TObjectA - это тот, который подсчитывается ссылкой, а не делегированный объект FObjectC.

Это по дизайну?

Edit

Я просто тестировал это в XE2, и поведение отличается. Второй оператор Supports возвращает nil для ZZ. Отладчик в XE4 говорит мне, что YY имеет в виду (TObjectA как IInterfaceY). В XE2 он говорит мне, что его a (указатель как IInterfaceY). Кроме того, в XE2 AA не пересчитывается на первый оператор поддержки, но внутренний FObjectC подсчитывается ссылкой.

Дополнительная информация после ответа на вопрос

Есть одна оговорка. Вы можете связать версию интерфейса, но не версию объекта. Это означает, что что-то вроде этого будет работать:

TObjectBase = class(TInterfacedObject, IMyInterface)
  …
end;

TObjectA = class(TInterfacedObject, IMyInterface)
  FMyInterfaceBase: IMyInterface;
  property MyDelegate: IMyInterface read GetMyInterface implements IMyInterface;
end;

function TObjectA.GetMyInterface: IMyInterface;
begin
  result := FMyInterfaceBase;
end;

TObjectB = class(TInterfacedObject, IMyInterface)
  FMyInterfaceA: IMyInterface;
  function GetMyInterface2: IMyInterface;
  property MyDelegate2: IMyInterface read GetMyInterface2 implements IMyInterface;
end;

function TObjectB.GetMyInterface2: IMyInterface;
begin
  result := FMyInterfaceA;
end;

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

TObjectBase = class(TInterfacedObject, IMyInterface)
  …
end;

TObjectA = class(TInterfacedObject, IMyInterface)
  FMyObjectBase: TMyObjectBase;
  property MyDelegate: TMyObjectBase read FMyObjectBase implements IMyInterface;
end;

TObjectB = class(TInterfacedObject, IMyInterface)
  FMyObjectA: TObjectA;
  property MyDelegate2: TObjectA read FMyObjectA implements IMyInterface;
end;

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

Ответы

Ответ 1

tl; dr Это все по дизайну - это просто изменение дизайна между XE2 и XE3.

XE3 и более поздние

Существует отличная разница между делегированием свойства интерфейса и делегированием свойства класса. В самом деле, документация вызывает эту разницу явно с разными разделами для двух вариантов делегирования.

Отличие от вашей перспективы заключается в следующем:

  • Когда TObjectA реализует IInterfaceY, делегируя свойство класса type CC, реализующий объект является экземпляром TObjectA.
  • Когда TObjectA реализует IInterfaceZ, делегируя свойство типа интерфейса BB, реализующий объект является объектом, реализующим FInterfaceB.

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

Чтобы увидеть это, измените определение кода TObjectC так:

TObjectC = class
public
  procedure DoNothing;
end;

Вы увидите, что этот код компилирует, запускает и ведет себя точно так же, как и ваша версия.

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

Итак, давайте посмотрим на ваши три звонка на Supports:

Supports(AA, IInterfaceY, YY);

Здесь реализующий объект AA, поэтому счетчик ссылок AA увеличивается.

Supports(YY, IInterfaceZ, ZZ);

Здесь реализующий объект является экземпляром TObjectB, поэтому его счетчик ссылок увеличивается.

Supports(ZZ, IInterfaceY, NewYY);

Здесь ZZ - это интерфейс, реализованный экземпляром TObjectB, который не реализует IInterfaceY. Следовательно, Supports возвращает False и NewYY is nil.

XE2 и ранее

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

Поведенческая разница, которую я могу найти, касается делегирования реализации интерфейса к свойствам типа класса. И особенно когда тип класса под вопросом поддерживает IInterface. В этом случае, в XE2, подсчет ссылок выполняется внутренним объектом. Это отличается от XE3, который имеет счетчик ссылок, выполняемый внешним объектом.

Обратите внимание, что для типа класса, который не поддерживает IInterface, подсчет ссылок выполняется внешним объектом во всех версиях. Это имеет смысл, поскольку для внутреннего объекта нет способа сделать это.

Вот мой примерный код, чтобы продемонстрировать разницу:

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  Intf1 = interface
    ['{56FF4B9A-6296-4366-AF82-9901A5287BDC}']
    procedure Foo;
  end;

  Intf2 = interface
    ['{71B0431C-DB83-49F0-B084-0095C535AFC3}']
    procedure Bar;
  end;

  TInnerClass1 = class(TObject, Intf1)
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    procedure Foo;
  end;

  TInnerClass2 = class
    procedure Bar;
  end;

  TOuterClass = class(TObject, Intf1, Intf2)
  private
    FInnerObj1: TInnerClass1;
    FInnerObj2: TInnerClass2;
  public
    constructor Create;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    property InnerObj1: TInnerClass1 read FInnerObj1 implements Intf1;
    property InnerObj2: TInnerClass2 read FInnerObj2 implements Intf2;
  end;

function TInnerClass1.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TInnerClass1._AddRef: Integer;
begin
  Writeln('TInnerClass1._AddRef');
  Result := -1;
end;

function TInnerClass1._Release: Integer;
begin
  Writeln('TInnerClass1._Release');
  Result := -1;
end;

procedure TInnerClass1.Foo;
begin
  Writeln('Foo');
end;

procedure TInnerClass2.Bar;
begin
  Writeln('Bar');
end;

constructor TOuterClass.Create;
begin
  inherited;
  FInnerObj1 := TInnerClass1.Create;
end;

function TOuterClass.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TOuterClass._AddRef: Integer;
begin
  Writeln('TOuterClass._AddRef');
  Result := -1;
end;

function TOuterClass._Release: Integer;
begin
  Writeln('TOuterClass._Release');
  Result := -1;
end;

var
  OuterObj: TOuterClass;
  I1: Intf1;
  I2: Intf2;

begin
  OuterObj := TOuterClass.Create;

  Supports(OuterObj, Intf1, I1);
  Supports(OuterObj, Intf2, I2);

  I1.Foo;
  I2.Bar;

  I1 := nil;
  I2 := nil;

  Readln;
end.

Выход на XE2:

TInnerClass1._AddRef
TOuterClass._AddRef
Foo
Bar
TInnerClass1._Release
TOuterClass._Release

Выход на XE3:

TOuterClass._AddRef
TOuterClass._AddRef
Foo
Bar
TOuterClass._Release
TOuterClass._Release

Обсуждение

Почему проект изменился? Я не могу ответить на этот вопрос окончательно, не будучи причастным к принятию решений. Однако поведение в XE3 мне лучше. Если вы объявите переменную типа класса, вы ожидаете, что ее время жизни будет управляться, как и любая другая переменная типа класса. То есть, путем явных вызовов деструктора на компиляторах на рабочем столе и ARC для мобильных компиляторов.

С другой стороны, поведение XE2 кажется непоследовательным. Почему должен использоваться тот факт, что свойство используется для делегирования реализации интерфейса, изменяя способ управления его жизненным циклом?

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

Пункты, приведенные выше, явно являются спекуляцией с моей стороны, но это лучшее, что я могу предложить!

Ответ 2

Вы смешиваете указатели объектов и указатели интерфейса, которые всегда являются рецептом катастрофы. TObjectA не увеличивает счетчик ссылок на его внутренние объекты, чтобы гарантировать, что они остаются в живых на протяжении всего его жизненного цикла, а TestInterfaces() не увеличивает счетчик ссылок AA, чтобы обеспечить его сохранение по всему набору тестов. Указатели объектов НЕ участвуют в подсчете ссылок! Вы должны управлять им вручную, например:

procedure TObjectA.AfterConstruction;
begin
  inherited;
  FObjectB := TObjectB.Create;
  FObjectB._AddRef;
  FObjectC := TObjectC.Create;
  FObjectC._AddRef;
  FObjectC.FTest := 'Testing';
end;

procedure TObjectA.BeforeDestruction;
begin
  FObjectC._Release;
  FObjectB._Release;
  inherited;
end;

AA := TObjectA.Create;
AA._AddRef;

Излишне говорить, что ручной подсчет ссылок подрывает использование интерфейсов.

При работе с интерфейсами вам необходимо либо:

  • Отключить подсчет ссылок полностью, чтобы избежать преждевременных разрушений. TComponent, например, делает именно это.

  • ВСЕГДА используйте указатели интерфейса, НИКОГДА с указателями объектов. Это обеспечивает правильный подсчет ссылок по всем направлениям. Обычно это предпочтительное решение.