6

我有两个类(在我的示例中为 TObject1 和 TObject2),它们通过接口(IObject1、IObject2)相互认识。正如您在 Delphi 中可能知道的那样,这将导致内存泄漏,因为两个引用计数器都将始终保持在零以上。通常的解决方案是将一个引用声明为弱引用。这在大多数情况下都有效,因为您通常知道哪个会首先被销毁,或者一旦被销毁就不一定需要弱引用后面的对象。

这就是说我试图以两种对象都保持活动状态直到不再引用它们的方式来解决问题:(Delphi 10.1 需要,因为我使用 [unsafe] 属性)

program Project14;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  IObject2 = interface;

  IObject1 = interface
    ['{F68D7631-4838-4E15-871A-BD2EAF16CC49}']
    function GetObject2: IObject2;
  end;

  IObject2 = interface
    ['{98EB60DA-646D-4ECF-B5A7-6A27B3106689}']
  end;

  TObject1 = class(TInterfacedObject, IObject1)
    [unsafe] FObj2: IObject2;
    constructor Create;
    destructor Destroy; override;

    function GetObject2: IObject2;
  end;

  TObject2 = class(TContainedObject, IObject2)
    [unsafe] FObj1: IObject1;
    constructor Create(aObj1: IObject1);
    destructor Destroy; override;
  end;

constructor TObject1.Create;
begin
  FObj2 := TObject2.Create(Self);
end;

destructor TObject1.Destroy;
begin
  TContainedObject(FObj2).Free;
  inherited Destroy;
end;

function TObject1.GetObject2: IObject2;
begin
  Result := FObj2;
end;

constructor TObject2.Create(aObj1: IObject1);
begin
  inherited Create(aObj1);
  FObj1 := aObj1;
end;

destructor TObject2.Destroy;
begin
  inherited Destroy;
end;

function Test1: IObject1;
var
  x: IObject2;
begin
  Result := TObject1.Create;
  x := Result.GetObject2;
end;

function Test2: IObject2;
var
  x: IObject1;
begin
  x := TObject1.Create;
  Result := x.GetObject2;
end;

var
  o1: IObject1;
  o2: IObject2;
begin
  try
    o1 := Test1();
    o2 := Test2();
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

这确实有效.. 函数 Test1 和 Test2 各自创建一个相互引用的 TObject1 和 TObject2 实例,一旦 o1 和 o2 超出范围,所有实例都会被销毁。该解决方案基于将引用计数转发给“控制器”(在本例中为 TObject1)的 TContainedObject。

现在我知道这个解决方案有缺陷,这就是我的问题开始的地方:

  • “TContainedObject(FObj2).Free;” 有点味道,但我没有更好的解决方案,因为我需要使用一个接口来引用 TObject2(生产代码为此包含一些继承)。有什么想法可以清理吗?
  • 您很容易忘记将 2 个类之间的所有引用声明为弱和 ..
  • 类似的问题随着更多的类而开始出现:让 TObject3 被一个引用并引用另一个:内存泄漏。我也可以通过让它从 TContainedObject 下降来处理它,但是对于遗留代码,这可能不是一件容易的事。

我觉得这个解决方案不能被普遍应用,并希望有一个可以 - 或者可能是一个答案来描述为什么很难甚至不可能有一个易于使用的 100% 解决方案来管理这种情况。恕我直言,拥有有限数量的对象并不会太复杂,一旦它们没有从其域外引用,它们就会相互破坏,而不必仔细考虑该域内的每个引用。

4

4 回答 4

1

Don't use unsafe
[unsafe] should not be used in normal code.
It is really a hack to the used if you don't want the compiler to do reference counting on interfaces.

Use weak instead
If for some reason you must have circular references then use a [weak] attribute on one of the references and declare the other reference as usual.

In your example it would look like this:

  TParent = class(TInterfacedObject, IParent)
    FChild: IChild;   //normal child
    constructor Create;
    function GetObject2: IChild;
  end;

  TChild = class(TContainedObject, IChild)
    //reference from the child to the parent, always [weak] if circular.
    [weak] FObj1: IParent;   
    constructor Create(const aObj1: IParent);
  end;

Now there is no need to do anything special in the destructors, so these can be omitted.
The compiler tracks all weak references and sets them to nil when the reference count of the referenced interface reaches zero.
And all this is done in a thread-safe manner.
However the weak reference itself does not increase the reference count.

When to use unsafe
This is in contrast to the unsafe reference, where no tracking and no reference counting at all takes place.

You would use an [unsafe] reference on an interfaced type that is a singleton, or one that has disabled reference counting.
Here the ref count is fixed at -1 in any case, so the calling of addref and release is an unneeded overhead.
Putting the [unsafe] eliminates that silly overhead.
Unless your interfaces override _addref and _release do not use [unsafe].

Pre Berlin alternative
Pre Berlin there is no [weak] attribute outside the NexGen compilers.
If you are running Seattle, 2010 or anything in between the following code would do {almost} the same.
Although I'm unsure if this code might not fall victim to race conditions in multithreaded code.
If that's a concern for you feel free to raise a flag and I'll investigate.

  TParent = class(TInterfacedObject, IParent)
    FChild: IChild;   //normal child
    constructor Create;
    function GetObject2: IChild;
  end;

  TChild = class(TContainedObject, IChild)
    //reference from the child to the parent, always [weak] if circular.
    FObj1: TParent;   //not an interface will not get refcounted.  
    constructor Create(const aObj1: IParent);
    destructor Destroy; override;
  end;

  constructor TChild.Create(const aObj1: IParent);
  begin
    inherited Create;
    FObject1:= (aObj1 as TParent);
  end;

 destructor TParent.Destroy;
 begin
   if Assigned(FChild) then FChild.InvalidateYourPointersToParent(self);
   inherited;
 end;

This will also ensure the interfaces get properly disposed, however now TChild.FObject1 will not automatically get nilled. You might be able to put code in the destructor of the TParent to visit all its children and inform them as in the code shown.
If one of the participants in the circular reference can't inform its weakly linked counterparts then you'll need to setup some other mechanism to nil those weak references.

于 2016-05-10T04:01:23.380 回答
0

如果你想让两个对象同时存活或死亡,那么它们肯定是一个对象。好的,我知道两者都可能是由不同的人开发的,所以我会让它们成为一个引用计数的超级对象的成员,就像这样

type
  TSuperobject = class( TInterfaceObject, IObject1, iObject2 )
  private
    fObject1 : TObject1;
    fObject2 : TObject2;
  public
    constructor Create;
    destructor Destroy;
    function GetObject2: IObject2;
    etc.
  end;

etc.

细节应该很明显。对 object1 或 object2 的任何引用都必须引用拥有对象( superobject.object1 等),因此 object1 和 object2 本身不需要进行引用计数 - 即它们可以是常规对象,而不是接口对象,但实际上并不重要如果它们被引用计数,因为所有者总是将引用计数加 1(在这种情况下,您可能不需要超级对象中的析构函数)。如果您将 object1 和 object2 作为被引用的对象,则它们对彼此的引用都很弱。

于 2016-05-10T13:55:26.523 回答
0

看起来您希望两个对象共享它们的引用计数。您可以通过让第三个对象 ( TPair) 处理引用计数来做到这一点。实现此目的的一个好方法是使用implements关键字。您可以选择隐藏这第三个对象,或与之交互。

使用下面的代码,您可以创建 a TPairChildA、 aTPairChildB或它们的 'parent' TPair。它们中的任何一个都会在需要时创建其他对象,并且所有创建的对象都将保持活动状态,直到不再引用任何对象。您当然可以将像您这样的接口添加IObject1到对象中,但为了简单起见,我将它们排除在外。

unit ObjectPair;

interface

type
  TPairChildA = class;
  TPairChildB = class;

  TPair = class( TInterfacedObject )
  protected
    FChildA : TPairChildA;
    FChildB : TPairChildB;

    function GetChildA : TPairChildA;
    function GetChildB : TPairChildB;
  public
    destructor Destroy; override;

    property ChildA : TPairChildA read GetChildA;
    property ChildB : TPairChildB read GetChildB;
  end;

  TPairChild = class( TObject , IInterface )
  protected
    FPair : TPair;

    property Pair : TPair read FPair implements IInterface;
  public
    constructor Create( APair : TPair = nil ); virtual;
  end;

  TPairChildA = class( TPairChild )
  protected
    function GetSibling : TPairChildB;
  public
    constructor Create( APair : TPair = nil ); override;

    property Sibling : TPairChildB read GetSibling;
  end;

  TPairChildB = class( TPairChild )
  protected
    function GetSibling : TPairChildA;
  public
    constructor Create( APair : TPair = nil ); override;

    property Sibling : TPairChildA read GetSibling;
  end;

implementation

//==============================================================================
// TPair

destructor TPair.Destroy;
begin
  FChildA.Free;
  FChildB.Free;
  inherited;
end;

function TPair.GetChildA : TPairChildA;
begin
  if FChildA = nil then
    FChildA := TPairChildA.Create( Self );
  Result := FChildA;
end;

function TPair.GetChildB : TPairChildB;
begin
  if FChildB = nil then
    FChildB := TPairChildB.Create( Self );
  Result := FChildB;
end;

// END TPair
//==============================================================================
// TPairChild

constructor TPairChild.Create( APair : TPair = nil );
begin
  if APair = nil then
    FPair := TPair.Create
  else
    FPair := APair;
end;

// END TPairChild
//==============================================================================
// TPairChildA

constructor TPairChildA.Create( APair : TPair = nil );
begin
  inherited;
  FPair.FChildA := Self;
end;

function TPairChildA.GetSibling : TPairChildB;
begin
  Result := FPair.ChildB;
end;

// END TPairChildA
//==============================================================================
// TPairChildB

constructor TPairChildB.Create( APair : TPair = nil );
begin
  inherited;
  FPair.FChildB := Self;
end;

function TPairChildB.GetSibling : TPairChildA;
begin
  Result := FPair.ChildA;
end;

// END TPairChildB
//==============================================================================

end.

一个使用示例:

procedure TForm1.Button1Click( Sender : TObject );
var
  objA : TPairChildA;
  ifA , ifB : IInterface;
begin
  objA := TPairChildA.Create;
  ifA := objA;
  ifB := objA.Sibling;
  ifA := nil;
  ifB := nil; // This frees all three objects.
end;
于 2016-05-31T13:27:36.197 回答
0

你在这里解决了错误的问题。

您的实际问题不在于强弱引用,也不在于如何改进您的解决方案。您的问题不在于如何实现,而在于您正在实现什么(想要实现)

首先直接解决您的问题:

  • “TContainedObject(FObj2).Free;” 有点味道,但我没有更好的解决方案,因为我需要使用一个接口来引用 TObject2(生产代码为此包含一些继承)。有什么想法可以清理吗?

你不能在这里做很多事情。您必须调用FreeFObj2因为TContainedObject它本身不是托管类。

  • 您很容易忘记将 2 个类之间的所有引用声明为弱和 ..

你也不能在这里做任何事情。它随领土而来。如果你想使用 ARC,你必须考虑循环引用。

  • 类似的问题随着更多的类而开始出现:让 TObject3 被一个引用并引用另一个:内存泄漏。我也可以通过让它从 TContainedObject 下降来处理它,但是对于遗留代码,这可能不是一件容易的事。

你也不能在这里做很多事情。如果你的设计真的是你想要的,那么你只需要处理它的复杂性。


现在,回到为什么你首先会遇到问题。

您想要实现的(并且您已经使用示例代码做到了)是通过抓取该层次结构中的任何对象引用来保持整个对象层次结构保持活力。

换种说法,你有Form和aButton就可以了,你想保持Form活力是某种东西持有a Button(因为Button它本身不会起作用)。然后你想添加EditForm并再次保持一切如果有东西抓住Edit

您在这里几乎没有选择。

  • 保留这种损坏的设计并使用您的解决方案,因为您涉及的代码太多,并且更改会很痛苦。如果你这样做了,请记住这最终会破坏设计,不要试图在其他任何地方重复它。

  • 如果您有层次结构,其中TObject1包含所有其他内容的根类,则重构它并继承TObject2TInterfacedObject以拥有自己的引用计数并且不要抓取对FObj2. TObject1如果你真的需要的话,取而代之的是获取根实例并传递它。

  • 这是第二种方法的变体。如果TObject1不是根类,则创建包含您需要的所有实例的附加包装类并传递该类。

最后两个解决方案远非完美,它们没有处理您可能有太多或类似的类的事实。但无论该代码多么糟糕,它甚至都无法接近您当前的解决方案。随着时间的推移,您可以慢慢地更改和改进这些解决方案,这比使用当前解决方案要容易得多。

于 2016-05-11T15:29:53.387 回答