Подсчет Ссылок На Интерфейс 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;

первый поддерживает вызов, который использует переменную в инструментах, возвращает YY, который на самом деле является ссылкой на TObjectA. Моя переменная AA-это подсчет ссылок. Потому что базовых пересчитать объект является TObjectA, второй поддерживает, который использует интерфейс в вызове supports, работает и возвращает мне интерфейс. Базовый объект теперь фактически является 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.

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

редактировать

Я только что протестировал это в XE2, и поведение отличается. Второй оператор Supports возвращает nil для ZZ. Отладчик в XE4 говорит мне, что YY ссылается на (TObjectA как IInterfaceY). В XE2 в он говорит мне, что его A (указатель как IInterfaceY). Кроме того, в XE2 AA не ref подсчитывается на первом операторе поддержки, но внутренний 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;

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

2 ответов


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

XE3 и позже

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

разница с вашей точки зрения является следующим образом:

  • , когда TObjectA осуществляет IInterfaceY делегируя свойству типа класса 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 и nil.

XE2 и раньше

изменения дизайна между XE2 и XE3 совпадают с введением компилятора mobile 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 есть послужной список не меняющегося поведения, если это не абсолютно необходимо.

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


вы смешиваете указатели объектов и указатели интерфейса, что всегда является рецептом катастрофы. 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;

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

при работе с интерфейсами, нужно либо:

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

  2. делать все с помощью указателей интерфейса, никогда с указателями объектов. Это обеспечивает правильный подсчет ссылок повсеместно. Это, как правило, предпочтительное решение.