Подсчет Ссылок На Интерфейс 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;
Излишне говорить, что ручной подсчет ссылок подрывает использование интерфейсов.
при работе с интерфейсами, нужно либо:
отключить подсчет ссылок полностью, чтобы избежать преждевременных разрушений.
TComponent
, например, делает именно это.делать все с помощью указателей интерфейса, никогда с указателями объектов. Это обеспечивает правильный подсчет ссылок повсеместно. Это, как правило, предпочтительное решение.