2014-03-26 3 views
15

오늘 뭔가 테스트하는 동안 이상한 상황에 처했습니다.Delphi Interface Reference Counting

많은 인터페이스와 개체가 있습니다. 코드는 다음과 같습니다

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; 

을 지금은이 같은 다양한 구현에 액세스하는 경우 나는 다음과 같은 결과를 얻을 : YY를

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; 

첫 번째 지지대가 구현에 변수를 사용하는 호출을 반환 이것은 실제로 TObjectA에 대한 참조입니다. 내 AA 변수는 참조 횟수입니다. 기본 참조 계산 개체는 TObjectA이므로 지원 호출에서 인터페이스를 사용하는 두 번째 지원은 작동하고 인터페이스를 반환합니다. 기본 객체는 실제로 이제는 TObjectB입니다. FInterfaceB 뒤에있는 내부 객체는 참조 카운트 된 객체입니다. 이 부분은 GetBB가 실제로 FInterfaceB이기 때문에 의미가 있습니다. 여기서 예상 한대로 Supports에 대한 마지막 호출은 NewYY에 대해 null을 반환하고 마지막에 호출이 실패합니다.

내 질문은 TObjectA에 대한 참조 계산이 첫 번째 지원 호출과 디자인에 의한 것입니까? 즉, 인터페이스를 구현하는 속성이 인터페이스가 아닌 객체를 반환하면 소유자 객체가 참조 계산을 수행하는 객체가됩니다. 나는 항상 내부 객체가 주 객체 대신에 참조 카운트되는 결과를 가져올 것이라는 인상을 받았다. 상기

이 옵션
property BB: IInterfaceZ read GetBB implements IInterfaceZ; 

가 FInterfaceB 뒤에 내부 오브젝트 레퍼런스 카운트 하나이다 : 다음

선언이다.

property CC: TObjectC read FObjectC implements IInterfaceY; 

위의 두 번째 옵션에서 TObjectA는 참조 카운트 된 것이고 위임 된 개체 FObjectC는 아닙니다. 이것은 의도적으로 설계된 것입니까?

편집

난 그냥 XE2에서이 테스트를 거쳐 동작은 다릅니다. 두 번째 Supports 문은 ZZ에 대해 nil을 반환합니다. XE4의 디버거는 YY가 (TObjectA를 IInterfaceY로 참조하고 있음을 나타냅니다. XE2에서 그것은 a (Pointer as 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; 

위임을 연결하려면 인터페이스를 고수하거나 다른 방법으로 사용해야합니다.

+0

당신은'TObjectA'에 대한 약한 참조만을 가지고 있습니다. 그러므로 당신은 AA를 잃고 나머지는 잃게 될 것입니다. –

+0

@SirRufo 나는 그것을 이해하고있다. 나는 참조 계산이 어떻게 행해지는지에 더 관심이있다. 기본적으로 어떤 개체가 참조 카운트되고 있습니다. – Graymatter

+0

_AddRef/_Release 메서드 호출을 무시하고 주 프로 시저뿐만 아니라 해당 호출도 기록하면 볼 수 있습니다. –

답변

17

TL; DR이 모든 설계에 의해 - 그것은 그냥 XE2와 XE3의 설계 변경이.

XE3 이후

클래스 타입 속성에 대한 인터페이스 유형 속성 및 위임에 위임 사이에 상당한 차이가 있습니다. 실제로 documentation은이 차이를 두 개의 위임 변형에 대해 서로 다른 섹션과 함께 명시 적으로 호출합니다.

  • TObjectA 클래스의 type 속성 CC에 위임하여 IInterfaceY를 구현하고, 구현하는 객체가 TObjectA의 인스턴스 다음과 같이

    관점의 차이입니다.

  • TObjectA이 인터페이스 유형 속성 BB에 위임하여 IInterfaceZ을 구현하는 경우 구현 객체는 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); 

ZZIInterfaceY를 구현하지 않는 TObjectB의 인스턴스에 의해 구현되는 인터페이스이다. 따라서 SupportsFalse이고 NewYYnil입니다.

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는이 문제를 검토하고 설계를 변경했습니다. Embarcadero는 절대적으로 필요한 경우를 제외하고는 동작을 변경하지 않았 음을 기록했기 때문에 ARC를 도입 할 때 디자인 변경이 필요했습니다.

위의 단락은 분명히 내 부분에 추측이지만, 그게 내가 제공해야 할 최고입니다!

+0

+1, "... 클래스 유형 속성 ... 인터페이스를 구현할 필요가 없습니다". 10:30 이전에 새로운 것을 배울 수있어서 좋았습니다. – iamjoosy

+0

@iamjoosy 예, 저 역시 새로운 것이 었습니다! –

+0

좋은 답변입니다. XE3의 동작이 더 낫다는 것에 동의합니다. 일관성이 있습니다. 또한 개발자가 참조 카운팅을 수행 할 위치를 선택할 수 있습니다. XE2 및 이전 버전에서는 내부 객체가 인터페이스 뒤의 기본 객체가되었습니다 (인터페이스를 구현할 때). 따라서 위임을 사용할 때 인터페이스에 대한 지원 사용이 제한됩니다. – Graymatter

3

항상 개체 포인터와 인터페이스 포인터를 혼합합니다.이 포인터는 항상 재난의 방법입니다. 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. 절대 인터페이스 포인터를 사용하고 객체 포인터를 사용하지 마십시오. 이렇게하면 보드 전체에서 적절한 참조 카운팅이 보장됩니다. 이것은 일반적으로 선호되는 솔루션입니다.

+0

이것은 테스트 코드의 작은 조각이었습니다. 나는 그 자체 또는 그와 비슷한 것을 드러내는 버그가 없다. 나는 인터페이스에 붙어있는 기본 객체가 무엇인지, 두 객체에서 참조되는 객체가 무엇인지에 더 관심이있다. 참조 계산과 관련된 문제를 이해합니다. AfterConstruction 메서드를보고 원래 예제를 참조하십시오. ref 카운트되지 않기 때문에 FObjectC에서 _AddRef를 수행 할 필요가 없습니다. 그게 내 질문에 관한거야. 내가 추가 한 코드에서, FObjectB는 참조 카운트이지만 FObjectC는 그렇지 않습니다. 질문이 정리되었습니다. – Graymatter

+0

XE2는이 코드에서 XE4와 다른 동작을 나타냅니다. 차이점을 보여주기 위해 질문을 업데이트했습니다. – Graymatter

+0

@Remy 당신이 말하는 모든 것이 정확하지만, 나는 그 질문을 정말로 이해했다고 생각하지 않습니다. –

관련 문제