不,永远不要在一次性类中实现终结器,因为它包装了一次性类。
假设您有一个带有终结器的类的三个清理场景Dispose
:
Dispose()
叫做。
- 在应用程序关闭时调用终结器。
- 该对象将被收集,但终结器没有被抑制(通常来自对 的调用
Dispose()
,但请注意,当任何事情使其处于不需要清理的状态时,您应该始终抑制终结器, 并在它处于确实需要它的状态时重新注册 - 例如,如果你有Open()
/Close()
对方法)。
现在,如果您直接管理非托管资源(例如,通过 的句柄IntPtr
),这三个场景中您将直接调用两个清理方法之一,这与您需要进行清理的三个场景相匹配。
好的。所以,让我们考虑一个一次性包装一次性用品,其中“外部”类有一个正确实现的终结器:
~MyClass()
{
// This space deliberately left blank.
}
finaliser 不做任何事情,因为没有非托管清理可供它处理。唯一的效果是,如果这个 null 终结器没有被抑制,那么在垃圾回收时,它将被放入终结器队列 - 保持它以及只能通过它的字段访问的任何东西,并将它们提升到下一代 - 最终完成线程将调用此 nop 方法,将其标记为已完成,并再次符合垃圾回收条件。但是自从它被推广以来,如果它是 Gen 0,它将是 Gen 1,如果它是 Gen 1,它将是 Gen 2。
确实需要最终确定的实际对象也将被提升,它不仅要等待更长时间,还要等待最终确定。无论如何,它将最终出现在第 2 代。
好吧,这已经够糟糕了,假设我们实际上在终结器中放入了一些代码,这些代码对包含可终结类的字段做了一些事情。
等待。我们会做什么?我们不能直接调用finaliser,所以我们dispose它。哦等等,我们确定这个类的行为Dispose()
和终结者的行为足够接近它是安全的吗?我们怎么知道它不会通过弱引用保留一些资源,它将尝试在 dispose 方法中而不是在 finaliser 中处理?该类的作者完全了解在终结器中处理弱引用的危险,但他们认为他们正在编写一个Dispose()
方法,而不是其他人的终结器方法的一部分。
然后,如果在应用程序关闭期间调用了外部终结器怎么办。你怎么知道内部终结器还没有被调用?当类的作者在编写他们的Dispose()
方法时,他们肯定会思考“好吧,现在让我们确保我处理这个在终结器已经运行之后被调用的情况,并且这个对象唯一剩下要做的就是拥有它内存释放了吗?” 并不真地。可能是他们提防反复呼吁Dispose()
以这种方式也可以避免这种情况,但你不能真正打赌(特别是因为他们不会在终结器中提供帮助,他们知道这将是最后一个调用的方法以及任何其他类型的清理,比如将不会再次用于标记它们的空字段是没有意义的)。它最终可能会做一些事情,比如删除一些引用计数资源的引用计数,或者违反它的工作要处理的非托管代码的合同。
所以。在此类中使用终结器的最佳情况是您破坏了垃圾收集的效率,更糟糕的是您的错误会干扰您试图帮助的完美清理代码。
还要注意模式背后的逻辑 MS 用来提升(并且仍然在他们的一些类中)你有一个受保护的Dispose(bool disposing)
方法。现在,我对这个模式有很多不好的地方要说,但是当你看到它时,它旨在处理这样一个事实,即你清理的Dispose()
内容和你在 finaliser 中清理的内容是不一样的——该模式意味着在这两种情况下都将清除对象的直接持有的非托管资源(在您的问题的场景中,没有此类资源),并且仅从IDisposable
以下位置清除托管资源(例如内部持有的对象),而不是来自终结者。Dispose()
IDisposable.Dispose()
如果您有任何需要清理的东西,无论是非托管资源、对象IDisposable
还是其他任何东西,请实施。
当且仅当您直接拥有需要清理的非托管资源时才编写终结器,并使清理它成为您在那里唯一要做的事情。
对于奖励积分,避免同时在两个类中 - 将所有非托管资源包装在仅处理该非托管类的一次性和可终结类中,如果您需要将该功能与其他资源组合,请在使用的一次性类中执行这样的课程。这样清理会更清晰、更简单、更不容易出现错误,并且对 GC 效率的损害更小(没有一个对象的最终确定延迟另一个对象的风险)。