我该如何编写正确的析构函数和终结器?

7

我正在尝试找出如何在C++/CLI中正确清理我的对象。

我已经阅读或浏览了这两篇文章(one, two),并查看了标准,还看了一些其他问题,特别是this one

我有各种信息:

  1. 最终器应清理非托管资源(因此在对象进行垃圾回收时,所有内容都会被清理)。
  2. 析构函数应清理托管资源(删除 Foo 还是 Foo.Dispose()?)并调用最终器(参见 1)。
  3. 析构函数和最终器都可以被多次调用(参见 3 第 8.8.8 页末尾)。
  4. 如果调用析构函数,则最终器将不再被调用(根据 1)(不是由 CLR 调用,也就是说,您仍然可以自己调用它)。
  5. 析构函数将调用基类析构函数(参见 3 第 25 页)。
  6. 具有最终器的类应始终具有析构函数(可能是为了确定性地清理非托管资源)。
  7. 对最终器的调用不会调用基类的最终器(3 第 131 页的 19.13.2)。

但是,部分原因造成了很多混乱:

  1. C#中的finalizers被称为destructors
  2. 析构函数在内部生成Dispose和Finalize方法(不确定Finalize),但Finalize方法不是finalizer
  3. C++中析构函数的语义不同,同时具有确定性清理和垃圾回收的复杂性。

我想要的答案是一个包含各种数据类型(托管、非托管、可处理的托管等)的类,并且拥有正确编写的析构函数和finalizer。

我还有两个更具体的问题:

  1. 通过在析构函数/终结器上设置bool hasBeenCleanedUp成员并将整个代码都放在条件语句中来处理可能被多次调用的情况是否可接受?
  2. 什么样的数据只能由析构函数清除,但不能在finalizer中清除,因为它可能已经被垃圾回收清除了?

如果您提供不同种类的数据示例并展示您的尝试,那么您将会得到更好的反馈。 - Matt Smith
1个回答

5
这不是对您问题的完整回答,但太长无法适合评论。
在一个完全托管的世界中,每个对象只引用托管对象,因此不需要finalizers或destructors,因为唯一的资源是内存,GC会处理它。
当你引用非托管资源时,你有责任在不再需要它们时释放它们。因此,你需要实现专门的清除代码。
有两种可能性:
1. 你知道何时不再需要非托管资源,所以可以确定地运行清除代码,这是通过析构函数/Dispose实现的; 2. 你不知道这些资源何时不再需要,所以你推迟到最后一刻进行清理,当封装资源的对象被GC收集时,这是通过finalizers实现的。
你猜想处于第一种情况要好得多,因为你不会占用更多的内存,避免了GC过程中的一些额外开销。
通常实现两者,因为实例的生命周期可能因使用而异。
在CLR级别上,没有确定性清除,只有finalizers。
在语言/API级别上,支持确定性清除:
1. 在本机C++中,当退出作用域或“删除”时调用析构函数; 2. 在.NET世界中,你有Dispose模式; 3. 在纯托管的C++/CLI世界中,析构函数映射到Dispose。
当你有机会确切地知道何时可以运行清除代码时,你调用(或让基础架构调用)析构函数。一旦清理完成,你可以摆脱所有的finalization过程,以便对象可以在下一个GC中立即被收集。
关于您第一系列观点的一些澄清:
1. 是的; 2. 析构函数也负责非托管资源的清除;它可以调用finalizer,如果你把清除代码因式分解在那里; 3. 技术上可以,但逻辑上应该使用简单的布尔保护来防止这种情况; 4. 是的,因为所有的清理工作都应该做完了,所以你请求CLR不要finalize对象; 5. 是的,因为基类知道它分配了哪些资源; 6. 是的,这是为了确定性清除; 7. 你应该确保这是你需要的情况。
其他的:
  1. 是的~ MyClass 被映射到 Finalize 方法的覆盖

  2. 如上所述,析构函数被映射到 Dispose,但你应该自己实现终结器: !MyClass

  3. 总结:C++ 析构函数和 Dispose 模式用于确定性清除,C# 析构函数、C++/CLI 终结器用于由 GC 触发的非确定性清除。


请注意,~MyClass 映射到 Dispose 方法,而 !MyClass 映射到 Finalize 方法。Dispose 方法由编译器自动生成,并调用 GC::SuppressFinalize,因此如果需要调用 finalized,则应在 ~MyClass 方法中显式调用 !MyClass。 - Zygfryd Wieszok

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接