两处引用接口对象

Reference to interfaced object in two pleaces

unit example;

interface

type
    ILettersSettings = interface
        function Letters: String;
    end;

    INumbersSettings = interface
        function Numbers: String;
    end;

    TSettings = class(TInterfacedObject, ILettersSettings, INumbersSettings)
    private
        fLoadedLetters: String;
        fLoadedNumbers: String;
    public
        procedure LoadFromFile;
    private {ILettersSettings}
        function Letters: String;
    private {INumbersSettings}
        function Numbers: String;
    end;

    TNumbers = class
    private
        fNumbers: String;
    public
        constructor Create(settings: INumbersSettings);
    end;

    TLetters = class
    private
        fLetters: String;
    public
        constructor Create(settings: ILettersSettings);
    end;


implementation

{ TSettings }

procedure TSettings.LoadFromFile;
begin
    fLoadedLetters := 'abc';
    fLoadedNumbers := '123';
end;

function TSettings.Letters: String;
begin
    result := fLoadedLetters;
end;

function TSettings.Numbers: String;
begin
    result := fLoadedNumbers;
end;

{ TNumbers }

constructor TNumbers.Create(settings: INumbersSettings);
begin
    fNumbers := settings.Numbers;
end;

{ TLetters }

constructor TLetters.Create(settings: ILettersSettings);
begin
    fLetters := settings.Letters;
end;

var
    settings: TSettings;
    letters: TLetters;
    numbers: TNumbers;
begin
    settings := TSettings.Create;
    settings.LoadFromFile;

    letters := TLetters.Create(settings);
    numbers := TNumbers.Create(settings);
end.

我对整个项目的设置有异议。

   settings := TSettings.Create;
   settings.LoadFromFile;

我用这个对象创建了两个对象:数字和字母,通过构造函数注入它。

letters := TLetters.Create(settings);
numbers := TNumbers.Create(settings);

但我没有将它分配给构造函数中的任何变量,只是使用它。

{ TNumbers }
constructor TNumbers.Create(settings: INumbersSettings);
begin
    fNumbers := settings.Numbers;
end;

{ TLetters }
constructor TLetters.Create(settings: ILettersSettings);
begin
    fLetters := settings.Letters;
end;

所以在构造函数开始时引用计数=1,在构造函数结束时引用计数减少到0,对象被销毁。 所以符合:

numbers := TNumbers.Create(settings);

注入 nil 并引发运行时错误。

如何解决?

有很多方法可以解决这个问题...最简单的方法可能是让 TSettings 继承自 TComponent 而不是 TInterfacedObject。

TComponent默认实现了IInterface但不实现引用计数,所以当引用计数减少时,对象不会被销毁。这也意味着你必须自己销毁它。

TSettings = class(TComponent, ILettersSettings, INumbersSettings)

[...]
settings := TSettings.Create;
try
  settings.LoadFromFile;

  letters := TLetters.Create(settings);
  numbers := TNumbers.Create(settings);
finally
  Settings.Free;
end;

问题是您混合了两种不同的生命周期管理方法。您混合了引用计数生命周期管理和程序员控制的生命周期管理。

您的变量 settings 被声明为 TSettings 类型。尽管您没有显示该声明,但我们知道这是因为您可以调用 LoadFromFile。这只有在 settings 被声明为 TSettings.

类型时才有可能

因为 settings 是 class,这意味着您的代码对其生命周期负责。因此,当您分配给 settings.

时,编译器不会发出引用计数代码

但是,当您调用 TLetters.CreateTNumbers.Create 时,您将接口引用分别传递给 ILettersINumbers。对于此代码,编译器确实发出引用计数代码。当您获得接口引用时,引用计数增加到 1,然后当该引用离开范围时减少到零。此时执行对象被销毁。

这一切的根本问题是你违反了生命周期管理规则。您不能像您所做的那样混合使用两种不同的方法。

人们通常采用的策略是要么始终使用程序员控制的管理,要么始终使用引用计数管理。这是你的选择。

如果您希望专门使用引用计数管理,那么您需要确保设置 class 的所有功能都可以通过接口使用。这意味着确保 LoadFromFile 可以通过接口调用。或者可能安排它由构造函数调用。

或者您可以切换到程序员控制的管理。在那种情况下,您不能从 TInterfacedObject 派生。您可能会像这样从 class 派生:

type
  TInterfacedObjectWithoutReferenceCounting = class(TObject, IInterface)
  protected
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;

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

function TInterfacedObjectWithoutReferenceCounting._AddRef: Integer;
begin
  Result := -1;
end;

function TInterfacedObjectWithoutReferenceCounting._Release: Integer;
begin
  Result := -1;
end;

但这也有其自身的风险。您必须确保在对象被销毁后您没有持有对该对象的任何引用。