我是否应该在包含TransactionScope的IDisposable类中使用finalize方法?

3
我编写了一个类,将TransactionScope与Linq to Sql的DataContext配对。
它实现了与TransactionScope相同的方法,即Dispose()Complete(),并公开DataContext。
它的目的是确保DataContexts不会被重复使用,它们与单个事务配对,并随其一起被处理。
我应该在类中包含一个Finalize方法吗?如果它还没有被调用,是否应该调用Dispose?还是只适用于引用非托管资源的IDisposable对象?
3个回答

3
不要仅仅因为一个类包含了另一个可处理的类,就在该类中实现终结器。考虑这样一个类,其中包括Dispose和终结器的三个清理场景:
1. 调用Dispose() 2. 应用程序关闭时调用终结器 3. 对象将被回收,但终结器未被禁止(通常是从调用Dispose()触发的,但请注意,在任何状态下都应禁用终结器,如果它不需要清理,则重新注册,如果它处于需要清理的状态-例如,如果您有一个Open()/Close()方法对)。
现在,如果您直接管理非托管资源(例如,通过IntPtr的句柄),那么这三种情况与您需要进行清理的三种情况匹配。
好的,现在让我们考虑一个正确实现了终结器的可处理包装器类。
~MyClass()
{
  // This space deliberately left blank.
}

最终器实际上是没有任何作用的,因为它没有未管理的清理处理。唯一的影响是,如果这个空的最终器没有被禁用,那么在垃圾回收时它将被放入最终器队列中,使其以及只能通过它的字段访问的任何东西保持活动状态,并将它们提升到下一代 - 最终线程最终会调用此nop方法,将其标记为已最终化,并且它再次符合垃圾回收条件。但由于它被提升了,所以如果之前它属于第0代,现在就属于第1代,而如果之前它属于第1代,则现在属于第2代。
需要最终化的实际对象也会被提升,它不仅需要等待进行回收,还需要等待进行最终化。无论如何,它最终都将进入第2代。
好了,这已经够糟糕了。让我们假设我们真的在最终器中放置了一些代码来处理持有最终化类的字段。
等等,我们该怎么做?我们不能直接调用最终器,所以我们要释放它。哦等等,我们确定对于这个类,Dispose()方法的行为和最终器的行为足够接近而且是安全的吗?我们怎么知道它是否通过弱引用持有某些资源,它将尝试在Dispose方法中而不是在最终器中处理这些资源?该类的作者非常清楚在最终器内部处理弱引用会带来的风险,但是他们认为自己写了一个Dispose()方法,并非其他人的最终器方法的一部分。
然后,如果应用程序关闭时调用外部最终器,该怎么办?您怎么知道内部最终器是否已经被调用过了?当该类的作者编写他们的Dispose()方法时,他们是否确定要考虑“好的,现在让我们确保我处理了这个对象在最终器运行之后被调用的情况,唯一剩下的事情就是释放它的内存”?实际上并没有。它可能是以一种防止重复调用Dispose()的方式进行保护,以便在这种情况下也能得到保护,但是您不能真正指望它(特别是因为它们不会在最终器中提供帮助,他们知道这是最后一次被调用的方法,任何其他形式的清理,例如将不再使用的字段置空以标记它们,都是毫无意义的)。它最终可能会执行一些操作,例如降低某个引用计数资源的引用计数,或者违反它需要处理的未管理代码的合同。
因此,在这样一个类中使用最终器的最好情况是破坏了垃圾回收的效率,最糟糕的情况是您会遇到一个干扰完全良好的清理代码的bug。
请注意微软推广的模式及其背后的逻辑(现在仍然有一些类中使用),其中包含了一个受保护的Dispose(bool disposing)方法。现在,我要说这种模式很多缺点,但是当你看它时,它的设计正是为了应对以下事实:用Dispose()清理和用终结器(finaliser)清理的资源并不相同,这种模式意味着对象的直接持有的非托管资源将在两种情况下都被清理(在你提问的场景中,不存在此类资源),而托管资源,例如内部持有的IDisposable对象,则仅从Dispose()中清理,而不是从终结器中清理。
如果您需要清理任何东西,无论是非托管资源、IDisposable对象还是其他任何东西,都请实现IDisposable.Dispose()
只有当您直接拥有需要清除的非托管资源时,请编写终结器,并确保在终结器中只清理该资源。
如果您想获得额外的分数,请避免同时处于两个类中 - 将所有非托管资源包装在可处理的和可终止的类中,这些类只与那些非托管类打交道,如果您需要将这种功能与其他资源结合使用,请使用仅可处理类来完成。这样清理将更加清晰、简单、不易出错,对GC效率的影响也会更小(没有一个对象的终结器会延迟另一个对象的终结器)。

3

Finalizer是专门用于清理非托管资源的。在finalizer内部调用相关对象的dispose没有任何意义,因为如果这些对象管理关键资源,则它们本身就有一个finalizer。

在.NET 2.0及以上版本中,甚至更少需要实现finalizer,因为.NET包含SafeHandle类。

然而,我有时仍然发现一个原因来实现finalizer,那就是查找开发人员是否忘记调用Dispose。我只让此类在调试构建中实现finalizer,并让它写入Debug窗口。


1
+1 这是一个很好的异常处理方式,我自己也用过。它不会导致终止错误,并且在调试运行中对性能的影响也可以接受。 - Jon Hanna

1

这个问题没有简单的答案 - 这是有争议的。

什么?

争论的焦点是是否使用带有完整可处理模式的终结器。您的代码使用事务和数据库上下文 - 这些人(通常)使用未管理的资源(如内核事务对象和TCP/IP连接)。

为什么?

如果您使用任何需要清理的未管理资源,则应实现IDisposable。然后,客户端代码可以将对类的调用包装到推荐的using(IDisposable myClass = new MyClass(){...}构造中。问题在于,如果开发人员不显式或隐式地调用IDisposable.Dispose(),则资源不会自动释放。即使对象myClass已被GC收集。这是因为GC在收集期间从不调用Dispose,而是由终结队列负责。

因此,您可以定义一个终结器,最终将由独立于垃圾回收的GC终结线程调用。

意见

有些人认为,你只需要确保将所有的“一次性”代码放入using (){}中,并忘记终止。毕竟,你必须尽快释放这些资源,而整个终止过程对于许多开发人员来说有点模糊。
相比之下,我更喜欢显式实现终止器,因为我不知道谁会使用我的代码。所以如果有人忘记在需要的类上调用Dispose,那么资源最终将被释放。
结论
个人而言,我建议对任何实现IDisposable的类实现终止器。

1
他们没有任何未受管理的资源,那么终结器应该做什么? - Jon Hanna
@JonHanna 事务范围通常使用内核事务对象,而数据上下文则持有 SQL 连接。这些基础的内部对象都使用非托管资源。 - oleksii
是的,但他们并没有实现TransactionScope,而是在TransactionScope周围实现了一个包装器。 TransactionScope本身是托管资源,无论它是否持有非托管资源,如果必要,它的终结器都将被调用。 最好的情况下,您只是延迟了它的终结和收集,最坏的情况下,您会引入一个错误(实际上,TransactionScope也不直接持有非托管资源,并且出于同样的原因,没有终结器 - 永远不要在一个不直接持有非托管资源的类中使用终结器)。 - Jon Hanna
同样地,DataContext 有一个相当大的可丢弃对象图,这些对象没有终结器,直到你到达 SQLConnection,它继承了一个终结器,但在构造时将其抑制,因为它从未被需要,它有一个内部对象(如果它是打开的),那个对象有一个终结器,因为它有一个非托管资源。任何那些在终结期间处理它的其他类都会再次最多减慢事情的速度,并可能引入错误。 - Jon Hanna

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