如何在Delphi中有效地使用内存管理接口



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

  1. 一般来说,我该如何使用它。创建接口和实现它的类。然后,每当我需要那个对象时,让变量实际上是interface类型,但实例化对象并presto?没有必要考虑解放它吗?终于不再尝试了?

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

  3. 哪些常见的陷阱可能会让我悲伤?例如:将接口对象强制转换为其类的对象会破坏我的引用计数吗?或者Delphi是否有任何不明显的方法可以创建引用循环?(意思是除了A使用B使用C使用A之外)

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

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

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

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

1) 您使用的副作用是为了COM兼容性而存在的。

2) 您正在使您的对象占地面积和效率变重。接口是指向指针列表的指针。。或者类似的东西。

3) 就像你说的。。。现在,您必须制作大量的接口,唯一的目的是避免自己释放内存。。。在我看来,这会带来更多的麻烦。

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

现在回答您的问题

1) 给定TFoo和接口IFoo,您可以有一个类似以下的方法

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

并且很快,你不需要最终释放它。

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

3) 2个问题。

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

B) 在所有引用发布之前释放对象,我无法强调这些bug是多么难以追踪。。。

在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()函数用作伪"声明",在创建任何对象之前,将任何和ALL AutoFree(()调用放置在方法的顶部,尽可能靠近它们引用的变量声明。

实现的全部细节,包括源代码和进一步的示例,都在我的博客上。

接口的内存管理是通过实现由TInterfacedObject实现的_AddRef和_Release来完成的。

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

  • 确保实现接口的类是从TInterfacedObject派生的,或者滚动您自己的祖先类,为_AddRef_Release提供良好的实现
  • 使用非此即彼:所以用户界面引用或对象实例引用都不要混合使用。在组件中实现接口时,这可能会产生问题(因为这些接口源自TComponent,而不是TInterfacedObject
  • 不要走TInterfacedComponent的路,因为它混合了基于所有者的内存管理和基于_AddRef/_Release的内存管理
  • 观察循环接口引用(您可以四处实现这里提到并在这里实现的"弱接口引用")
  • 您需要维护额外的代码,因为您需要为类中要公开的部分定义接口,并保持这两个部分的同步(为此,您可以使用ModelMaker代码浏览器;它允许您提取接口,并通常促进您的开发,因为它在单次操作中管理代码的接口/实现部分)
  • 您需要一些额外的管道来创建基础类的实例。你可以使用工厂模式

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

最简短的答案:默认的delphi内存模型是所有者释放他们自己的对象。所有其他引用都是弱引用,必须在所有者释放之前释放。"共享"一个使用寿命短于应用程序整个使用寿命的对象是很少实现的。很少进行引用计数,而且当进行引用计数时,只能由专家进行,否则会添加比解决更多的错误和崩溃。

学习惯用的delphi风格并尝试模仿它,不要与谷物作斗争。可悲的是,人们认为"针对接口而非实现的程序"意味着"在任何地方都使用IUnknown"。这不是真的。我建议您不要使用COM IUnknown接口,而是使用抽象基类。唯一不能做的就是在一个类中实现两个抽象基类,而且很少需要这样做。

更新:我最近发现使用COM接口(基于IUnknown)帮助我将模型和控制器实现与UI类分离是很有帮助的。所以我确实发现使用基于IUnknown的接口很有用。但是,没有太多的文档和现有技术可以作为你工作的基础。我希望看到一个"食谱"风格的食谱,为人们列出所有这些,这样他们就可以在没有通常的界面和非基于界面的终身管理相结合的问题,以及当你习惯了这种额外的复杂性时所带来的所有麻烦的情况下工作。

切换到仅用于避免手动Free的接口是毫无意义的。Free/try-finally行中的少量经济性很难弥补在接口中同时声明g/setter和属性的必要性,而不提及保持intf/class声明同步的必要性。由于隐式的最终确定代码和引用计数,接口也会带来性能损失。如果性能不是重点,而您想要实现的只是自动刷新,我建议使用一些通用接口包装器,如Deltics建议的那样。

最新更新