【问题标题】:How to effectively use interfaces for memory management in DelphiDelphi中如何有效地使用接口进行内存管理
【发布时间】:2012-07-31 20:38:12
【问题描述】:

我对 Delphi 相当陌生,并且一直在手动进行所有内存管理,但听说过 Delphi 能够使用接口进行引用计数并以这种方式提供一些内存管理的引用。我想开始,但有几个问题。

  1. 一般来说,我该如何使用它。创建接口和实现它的类。那么每当我需要该对象时,变量实际上是否为接口类型,但实例化该对象并立即执行?不用考虑释放它吗?没有更多的try-finallys?

  2. 为真正不需要的类创建一堆接口似乎很麻烦。关于自动生成这些的任何提示?我如何最好地组织它?接口和类在同一个文件中?

  3. 可能导致我悲伤的常见陷阱有哪些?例如:将接口对象转换为其类的对象会破坏我的引用计数吗?还是有任何不明显的方式 Delphi 会创建参考循环? (意思是除了A用B用C用A)

如果有涵盖所有这些的教程,那就太好了,但我在搜索中没有找到任何东西。谢谢。

【问题讨论】:

  • 只是 MHO:继续手动管理内存!
  • 想详细说明为什么?优点/缺点?
  • 你可以从doc wiki开始;该页面有大约十几个不同的链接。如果您使用[delphi] interfaces 搜索 SO,还有一些有用的相关问题(只需在任何页面右上角的搜索栏中输入)。这里的问题应该简短而简洁,所以你可以在阅读一些有更具体问题的文档后回来,这里有人可以帮助你得到答案。 :-)
  • 关于优点/缺点:只有一个 Pro - 您不必在适当的地方免费拨打电话。缺点 - 许多手动完成时根本不存在的问题。你已经提到了一些,GDF 提到了一些,还有很多。
  • @Uwe:我认为正确的方法没有任何问题。不要把你的对象变成接口,使用使用接口的守卫来保护你的对象。这样,您可以像往常一样使用您的对象,但不必(必须)释放它们。正如我所说,如果做得好,没有缺点。这是一种在其他语言中一直使用的技术。

标签: delphi delphi-xe


【解决方案1】:

我目前正在处理一个非常大的项目,该项目利用接口引用计数的“副作用”来进行内存管理。

我个人的结论是,您最终会得到很多过于复杂的代码,没有比“我不必担心免费调用”更好的理由

出于一些非常基本的原因,我强烈建议不要采取这种行动:

1) 您正在使用出于 COM 兼容性目的而存在的副作用。

2) 您正在增加对象的占用空间和效率。接口是指向指针列表的指针……或类似的东西。

3) 就像您所说的那样...您现在必须制作成堆的接口,其唯一目的是避免自己释放内存...在我看来,这会带来更多的麻烦。

4) 当一个对象被释放,在它被引用之前,最常见的 bug 将成为一个巨大的调试痛苦。我们在自己的引用计数中有特殊的代码,可以在软件推出之前尝试和测试这个问题。

现在回答您的问题。

1) 给定 TFoo 和接口 IFoo 你可以有如下方法

function GetFoo: IFoo;
begin
  Result := (TFoo.Create as IFoo);
end;

...presto,你不需要 finally 来释放它。

2) 是的,就像我说的,你认为这是一个好主意,但它变成了对 bupkis 的巨大痛苦

3) 2 个问题。

A) 你有 Object1.Interface2 和 Object2.Interface1...由于循环引用,这些对象永远不会被释放

B) 在释放所有引用之前释放对象,我无法强调这些错误有多难追踪...

【讨论】:

  • 将一个类变成一个仅用于内存管理的接口确实并不总是一个好主意,它确实可能是一个性能问题。但是正如 Deltics 所提到的,监护人界面没有任何问题。 RAII 存在同样的副作用,并且没有性能问题,因为您直接使用该类。
  • +1,不是为了回答这个问题,而是为了建议不要使用它;)
  • 谢谢。这个和这里的许多其他人已经让我相信,处理一些尝试/最后的事情并没有那么糟糕。
  • @RudyVelthuis 我同意你的观点,但是 Deltics 的例子虽然很优雅,但只有当我在该代码段中创建和使用对象时才具有你提到的优点。如果我必须创建一个全局对象并传递它,那么 Deltics 技术将不再有效。据我所知,使用 Deltics 解决方案将我的代码从 4 行减少到 2 行,并将对象的生命周期保持在代码块内。我在这里看不到真正的物有所值,但这只是我......
  • @GDF:如果必须是全局的,那么使用全局变量。并且物有所值:您不能忘记释放对象,这是最常犯的错误之一。方法越长,你就越有可能忘记它。 RAII(通常称为 Deltic 的技术)是一种非常有价值的编程习惯。哦,根本不需要释放这些对象。 RAII 对象会处理它。
【解决方案2】:

导致 Delphi 中“自动垃圾收集”的最常见抱怨是,即使是短暂的临时对象也必须手动处理,并且您必须编写大量“样板”代码以确保在发生异常时发生这种情况。

例如,为某个过程中的某些临时排序或其他算法目的创建一个 TStringList

procedure SomeStringsOperation(const aStrings: TStrings);
var
  list: TStringList;
begin
  list := TStringList.Create;
  try
      :
     // do some work with "list"
      :
  finally
    list.Free;
  end;
end;

正如您所提到的,实现引用计数生命周期管理的 COM 协议的对象通过在释放对它们的所有引用时清理自己来避免这种情况。

但由于 TStringList 不是 COM 对象,您无法享受它提供的便利。

幸运的是,有一种方法可以使用 COM 引用计数来处理这些事情,而无需创建您希望使用的类的所有新 COM 版本。您甚至不需要切换到完全基于 COM 的模型。

我创建了一个非常简单的实用程序类,允许我将任何对象“包装”在一个轻量级 COM 容器中,专门用于获得这种自动清理行为。使用这种技术,您可以将上面的示例替换为:

procedure SomeStringsOperation(const aStrings: TStrings);
var
  list: TStringList;
begin
  AutoFree(@list);

  list := TStringList.Create;

    :
  // do some work with "list"
    :
end;

AutoFree() 函数调用创建一个“匿名”接口对象,该对象在编译器为该过程生成的退出代码中Release()。这个自动释放对象被传递一个指向变量的指针,该变量引用您希望被释放的对象。除此之外,这允许我们使用 AutoFree() 函数作为伪“声明”,将任何和所有 AutoFree() 调用放在方法的顶部,尽可能靠近变量他们引用的声明,甚至在我们创建任何对象之前。

实现的全部细节,包括源代码和更多示例,都在我的博客this post

【讨论】:

  • @David:确实很好,但不是新的。多年前,我向 JEDI 捐赠了一个类似的解决方案。 Barry Kelly 在他的博客中对同一原则有一些更好的实现。
  • 鲁迪,我从未声称它是“新的”,只是将其作为对 OP 问题的回答。您还会注意到,我引用的我的博客文章本身是多年前发布的。事实上,BK 自己对此发表了评论,指出他的解决方案有不同的目标,并承认他的“智能指针”存在使用方便性方面的问题。在寻求减轻负担(OP 的目标)时,用另一组令人讨厌的负担代替一组令人讨厌的负担通常只会导致复杂性的增加,而讨厌的净减少为零。 ;)
  • @Deltics,如何在列表变量上创建多个 TStringList 而没有正确释放它。它会导致泄漏,对吗?因此,我认为像这样的伪解决方案是脆弱的。如果我错了,请纠正我。
  • AutoFree 不是作为垃圾收集的包罗万象的解决方案提供的,只是作为一种减少使用显式 try..finally 所涉及的样板数量的方法。程序员错误地重用/覆盖变量的问题同样适用于两者。 AutoFree 不会尝试消除该问题,但它也不会创建该问题。
【解决方案3】:

接口的内存管理是通过_AddRef_Release的实现来完成的,TInterfacedObject实现了。

一般来说,使用接口来减少内存管理的繁琐可能是个好主意,但您需要注意以下几点:

  • 确保实现接口的类派生自 TInterfacedObject 或滚动您自己的祖先类,为 _AddRef_Release 提供良好的实现
  • 使用非此即彼:所以无论是用户界面引用,还是使用对象实例引用,都不要混合使用。在组件中实现接口时可能会出现问题(因为这些接口源自TComponent,而不是TInterfacedObject
  • 不要采用TInterfacedComponent 方式,因为它混合了基于Owner 的内存管理和基于_AddRef/_Release 的内存管理
  • 观看循环接口引用(您可以绕过实现“弱接口引用”提到here 并实现here
  • 您需要维护额外的代码,因为您需要为要公开的类的部分定义接口,并使这两个部分保持同步(您可以Model Maker Code Explorer 这样做;它允许您提取接口,一般来说促进您的开发,因为它以单一动作管理代码的接口/实现部分)
  • 您需要一些额外的管道来创建底层类的实例。你可以use the factory pattern for that

这并不总是有效,但确实回答了您的一些潜在问题。

【讨论】:

    【解决方案4】:

    最短的可能答案:默认的 delphi 内存模型是所有者释放他们拥有的对象。所有其他引用都是弱引用,必须在所有者之前放手。很少会“共享”一个生命周期短于应用程序整个生命周期的对象。引用计数很少进行,而且一旦完成,也只有专家才能完成,否则会增加比解决的更多的错误和崩溃。

    学习惯用的 delphi 风格并尝试模仿它,不要争吵。可悲的是,人们认为“针对接口而不是实现的程序”意味着“到处使用 IUnknown”。这不是真的。我建议您不要使用 COM IUnknown 接口,而是使用抽象基类。唯一不能做的就是在一个类中实现两个抽象基类,这种需求很少见。

    更新:我最近发现使用 COM 接口(基于 IUnknown)帮助我从 UI 类中分离出我的模型和控制器实现很有帮助。所以我确实发现使用基于 IUnknown 的接口很有用。但是没有很多文档和现有技术可以作为您努力的基础。我希望看到一个“食谱”风格的食谱,它为人们列出了所有这些,这样他们就可以在没有结合界面和基于非界面的生命周期管理的常见问题的情况下工作,以及在你习惯时遇到的所有麻烦额外的复杂性。

    【讨论】:

      【解决方案5】:

      仅仅为了避免手动Free而切换到界面是没有意义的。 Free/try-finally 行的经济性几乎无法弥补在接口中同时声明 g/setter 和属性的必要性,更不用说保持 intf/class 声明同步的必要性了。由于隐式完成代码和引用计数,接口也会带来性能损失。如果性能不是重点,而您想要实现的只是自动释放,我建议使用一些通用接口包装器,例如 Deltics 建议的那种。

      【讨论】:

        猜你喜欢
        • 2014-06-16
        • 2021-08-08
        • 2015-12-27
        • 1970-01-01
        • 2014-01-12
        • 2011-03-07
        • 2011-02-02
        • 1970-01-01
        • 2011-08-22
        相关资源
        最近更新 更多