Delphi 中接口的内存管理

Memory management of interfaces in Delphi

我正在努力学习 delphi 和内存管理,来自 C#。

这种斗争的当前体现是,当我处理完这些对象时,我不知道正确的处理方法。从阅读和我的实验看来,如果我有一个被转换为接口的对象,那么我唯一的选择是将引用设置为 nil。 如果我去打电话 免费和零() 我最终遇到访问冲突,例如:

var
  foo: IFoo;
begin
  foo := TFoo.Create();
  FreeandNil(foo);
end;

当然,我需要做的就是改变 foo:IFoo;至 foo:TFoo;它很快乐。或者简单地将指针设置为 nil,而不是调用 freeandNil。

foo := nil;

所以,在一个层面上,我一点也不明白 AV 在哪里。

在不同的层面上,我希望编写的代码不需要知道它是接口还是对象。我希望能够以完全相同的方式编写我所有的内存管理,但我似乎无法编写可以处理 Class 或接口的方法。好吧,那不是真的,我确实有一些东西,但是它太丑了我犹豫了 post 它。

但我想我也应该问,其他人都在做什么?脑子里记着什么是接口,只是 nil 那些指针?否则调用 FreeAndNil?

我打算第一次将事情实现为一个具体的 class,但当我发现代码可以通过 2 种不同的方式执行某些操作时,稍后回来将其更改为接口.而且我不想检查代码并更改它处理该引用的方式,这是我当时最不想考虑的事情。

但为了讨论,我最好的(几乎唯一的)想法是 class:

interface

type
  TMemory = class(TObject)
  class procedure Free(item: TObject); overload; static;
  class procedure Free<T: IInterface>(item: T); overload; static;
  end;

implementation

uses
  System.SysUtils;

  { TMemory }

class procedure TMemory.Free(item: TObject);
begin
  FreeandNil(item);
end;

class procedure TMemory.Free<T>(item: T);
begin
  //don't do anything, it is up the caller to always nil after calling.
end;

那么我就可以一直调用:

TMemory.Free(Thing);
Thing := nil;

测试代码:

procedure TDoSomething.MyWorker;
var
  foo: IFoo;
  fooAsClass: TFoo;
  JustAnObject: TObject;
begin
  foo := TFoo.Create();
  fooAsClass := TFoo.Create();
  JustAnObject := TObject.Create();

  TMemory.Free(foo);
  foo := nil;

  TMemory.Free(fooAsClass);
  fooAsClass := nil;

  TMemory.Free(JustAnObject);
  JustAnObject := nil;
end;

运行时没有泄漏或访问冲突。 (使用 MadExcept)

但是非常感谢 Delphi SO 社区。你们是最棒的学习对象!

如果我们通过接口变量访问某个对象,并不总是意味着对象在引用计数器降为零时就被销毁了。例如,TComponent 方法 _AddRef 和 _Release 实现是 'dummy':没有实现引用计数并且 TComponent 永远不会被销毁,因为接口变量超出范围。

为了实现我们对 'real' 接口的预期行为,您的所有对象都应该是 TInterfacedObject 的后代,或者您需要自己实现 _AddRef / _Release

是的,有 2 种不同的内存管理方法,它们通常共存于一个程序中,但只有在以两种方式处理同一对象时才会出现混淆(和 AV)。如果我们销毁了对象,然后接口变量就超出了范围,它们会调用销毁对象的 _Release 方法,这会导致访问冲突。这是一项有风险的业务,但只要稍加注意,它是可行的。

经典 Delphi 组件不进行引用计数,而是使用所有权的概念。每个组件都有一个所有者,其职责是在它自身被销毁时释放所有内存。所以每个组件都有一个所有者,但它也可能有很多指向另一个组件的指针,比如 Toolbar 有 ImageList 变量。如果这些组件被重新计数,它们将永远不会因为循环引用而被销毁,所以为了打破这个循环,你还需要 'weak' 引用而不是 'count'。他们也在这里,但那是 Delphi 的最新功能。

如果你的对象中有一些层次结构,所以你知道 'bigger' 对象需要所有 'smaller' 对象才能起作用,那么使用这个很好的旧方法,它非常简单并且非常好Delphi 中的实现,即:无​​论在何处出现异常,您都可以编写无泄漏的代码。有所有这些小事情,比如使用 .Free 而不是 .Destroy,因为如果在构造函数中发生异常,析构函数会自动调用,等等。事实上非常聪明的解决方案。

只有当您不知道某个对象需要多长时间并且没有合适的 'owner' 时,我才会使用 refcounted 接口。我用一个线程中保存到文件的扫描图像来完成它,同时转换为较小的图像以在另一个线程的屏幕上显示。当一切都完成后,RAM 中不再需要图像并且可以被销毁,但我不知道哪个先发生。在这种情况下,最好使用引用计数。

访问冲突的原因是 FreeAndNil 采用 未类型化 参数,但 需要 它是一个对象。所以方法是对对象进行操作的。

procedure FreeAndNil(var Obj);
var
  Temp: TObject;
begin
  Temp := TObject(Obj); //Obj must be a TObject otherwise all bets are off
  Pointer(Obj) := nil; //Will throw an AV if memory violation is detected
  Temp.Free; //Will throw an AV if memory violation is detected
end;

如果您销毁一个先前已销毁或从未创建的对象,则上述 中的内存违规可能 (不保证注意)会被检测到。如果 Obj 根本不引用对象而是引用其他对象(例如接口、记录、整数,因为它们没有实现 Free 并且如果它们实现了,它也可能会被检测到与 TObject.Free).

的定位方式不同

On a differently level, I want to write the code such that it does not need to know if it is an interface or an object. I want to be able to write all of my memory management the same exact way.

这就像说您想以与淋浴完全相同的方式使用汽车。
好吧,也许区别不是那么极端。但重点是接口和对象(以及与此相关的记录)使用不同 内存管理范例。你不能以同样的方式管理他们的内存。

  • 需要明确销毁对象。您可以使用所有权模型,但销毁仍然是一个明确的外部操作。
  • 接口是引用计数的。编译器注入代码来跟踪引用(查看)底层实例的字段和变量的数量。通常,当最后一个引用被释放时,对象会自行销毁。 (有一些方法 超出了这个答案的范围 来改变它。)