实现Finalize和Dispose的正确方法(当父类实现IDisposable接口)

4
我正在实现我的类中的Finalize和Dispose,我在父类上实现了IDisposable并覆盖了子类中的Dispose(bool)重载。我不确定:

  1. 是否应该使用重复的isDisposed变量(因为它已经存在于基类中)?
  2. 是否也应在子类中实现finalizer?

这两件事在此示例中都已完成 -

http://guides.brucejmack.biz/CodeRules/FxCop/Docs/Rules/Usage/DisposeMethodsShouldCallBaseClassDispose.html

然而,这篇MSDN文章中的示例没有这两个内容 - http://msdn.microsoft.com/zh-cn/library/b1yfkh5e.aspx

而这个MSDN中的示例是不完整的 - http://msdn.microsoft.com/zh-cn/library/ms182330.aspx


可能是 C# Finalize/Dispose pattern 的重复问题。 - Øyvind Bråthen
@Øyvind Bråthen:这个问题是相关的,但它没有讨论继承情况下的这种模式。 - akjoshi
顺便提一下,我注意到这个问题被标记为wpf标签,然而实现IDisposable通常对于基于WPF的控件并不必要,因为它们不像Windows Forms控件那样固有地拥有资源,例如专用HWND。 - jpierson
@jpierson:我同意,但我正在我的WPF应用程序中实现它,因此标记了它,以防有关于WPF的特定/新内容。 - akjoshi
我不确定为什么这个问题会被投票'关闭'!我还不清楚两种方法中哪一个是正确的,这里给出了矛盾的答案,而没有一个清楚地提到每种方法的优缺点?如果您在投票后能提供一条评论,我将非常感激。 - akjoshi
5个回答

6
很少有情况下终结器是有用的。您链接的文档并没有提供完全有用的信息,它只给出了以下相当循环的建议:“仅在需要终结的对象上实现Finalize”。这是一个很好的例子,但并不是非常有用。实际上,在大多数情况下,您都不需要终结器。(.NET开发人员必须经历的学习曲线之一是发现他们认为需要终结器的大多数地方实际上并不需要。)您将其标记为(其他内容之一)WPF问题,我会说在UI对象上放置终结器几乎总是错误的。(因此,即使您处于需要终结器的不寻常情况之一,该工作也与涉及WPF的代码无关。)对于大多数似乎可能有用的终结器场景,它们最终都不是有用的,因为在终结器运行时,它已经为时已晚。例如,通常尝试使用对象引用的任何对象进行任何操作都是不明智的,因为在终结器运行时,这些对象可能已经被终结。(.NET不保证终结器运行的顺序,因此您根本无法知道您引用的对象是否已被终结。)在已运行终结器的对象上调用方法是不明智的。如果您有某种方式可以知道某个对象肯定没有被终结,那么使用它是安全的,但这是一个相当不寻常的情况。(除非所涉及的对象没有终结器,并且自身不使用任何可终结的资源。但在这种情况下,您可能不需要在自己的对象消失时对其执行任何操作。)终结器似乎有用的主要情况是Interop:例如,假设您正在使用P / Invoke调用一些未管理的API,并且该API返回给您一个句柄。也许还有其他API需要调用以关闭该句柄。由于这都是未管理的内容,因此.NET GC不知道这些句柄是什么,而您的工作就是确保它们得到清理,在这种情况下,终结器是合理的……实际上,最好始终使用SafeHandle来处理该场景。在实践中,我发现自己使用终结器的唯一地方是a)旨在研究GC行为的实验和b)旨在发现有关系统中如何使用特定对象的诊断代码。这两种代码都不应进入生产环境。因此,“是否需要在子类中实现终结器”这个问题的答案是:如果您需要问,那么答案是否定的。
关于是否要复制标志...其他答案在这里提供了矛盾的建议。主要点是1)您确实需要调用基本Dispose和2)您的Dispose需要是幂等的(即,无论它被调用一次、两次、5次、100次——如果它被调用多次,它不应该抱怨)。您可以自由地按照自己的方式实现这一点——布尔标志是一种方法,但我经常发现,在我的Dispose方法中将某些字段设置为null就足够了,此时就不需要单独的布尔标志了——您可以告诉Dispose已经被调用,因为您已经将那些字段设置为null
很多有关IDisposable的指导都极其无用,因为它解决的是需要终结器的情况,但这实际上是非常罕见的情况。这意味着许多人编写的IDisposable实现比必要的复杂得多。实际上,大多数类调用Stephen Cleary称之为“级别1”的类别,在这个jpierson链接到的文章中,这些类别不需要所有的GC.KeepAliveGC.SuppressFinalizeDispose(bool)等杂乱无章的东西。大多数时候,生活实际上要简单得多,正如Cleary针对这些“级别1”类型的建议所示。

谢谢Ian,非常有用。 我有几个问题:1.我们可以使用基类标记(因为我们正在调用base.Dispose并且它会负责设置标记)吗?2.对于UI对象,仅实现IDisposable就足够/建议了(不需要finalizer)吗? - akjoshi
我同意Ian的观点,即不一定需要布尔字段。我建议遵循这个惯例是一个好主意,但在某些情况下,如果您有一个字段,如果为空,已经意味着Dispose已发生。 - jpierson
如果一个对象对于清理而言并不需要引用其他对象,那么它不应该被作为可终结的。相反,用于清理的内容应该被封装在自己的可终结对象中,这些对象不应该引用任何不必要的内容。 - supercat

2

需要复制

如果子类中没有清理工作,只需调用base.Dispose(),如果有一些类级别的清理工作,则在调用base.Dispose()后执行。您需要将这两个类的状态分开,因此每个类都应该有一个IsDisposed布尔值。这样,您可以随时添加清理代码。

当您确定一个类为IDisposable时,您只需告诉GC我正在处理其清理过程,并且您应该SuppressFinilize这个类,这样GC会将它从队列中删除。除非您调用GC.SupressFinalize(this),否则对于IDisposable类不会发生特殊事件。因此,如果按照我提到的实现它,就不需要Finilizer,因为您刚刚告诉GC不要对其进行终结。


那么您建议使用第一个链接(FxCop文档)中提供的模式吗? - akjoshi
谢谢Tomas,我也认为这是一个好方法,但MSDN文章说(对于派生类)-“此派生类没有Finalize方法或没有参数的Dispose方法,因为它从基类继承了它们。”这是什么意思? - akjoshi
2
虚拟值-1。你不需要一个副本。对象要么处于处理状态,要么处于完成状态。基类不是另一个对象,它只是在同一对象上的基本行为。它要么正在处置,要么正在完成。 - Aliostad
@akjoshi:这意味着派生类不需要特殊的清理,因此不需要为IDisposable实现特殊的实现。如果我们为每个类实现一个空模式,那么就会浪费更多时间。因此,如果您在派生类中没有特殊的清理,请不要覆盖基类。基类Dispose将自动调用。但是,如果您覆盖了它,请记得调用base.Dispose() - Tomas Van Joseph
@Aliostad:如果我们无法访问基类代码怎么办?您需要在类级别而不是对象级别上分离“disposing/finalizing”的行为。 - Tomas Van Joseph

1

实现IDisposable的正确方式取决于您的类是否拥有任何非托管资源。实现IDisposable的确切方式仍然是一些开发人员意见不一致的问题,一些人像Stephen Cleary那样对一般的可释放模式持有强烈观点。

参见:实现Finalize和Dispose以清理非托管资源

IDisposable接口的文档也简要说明了这一点,本文也指出了一些相同的内容,但也在MSDN上进行了介绍。

关于基类是否需要一个重复的布尔字段“isDisposed”。看起来这主要是一个有用的约定,当子类本身可能添加额外的未托管资源需要处理时可以使用。由于Dispose被声明为虚拟方法,在子类实例上调用Dispose总是导致该类的Dispose方法首先被调用,然后调用base.Dispose作为其最后一步,给每个继承层次结构中的级别清理的机会。因此,我可能会总结为,如果您的子类具有超出基类所拥有的其他未托管资源,则最好在其Dispose方法内跟踪其处置的事务性自己的布尔isDisposed字段,但正如Ian在他的答案中提到的那样,还有其他表示已处置状态的方法。


感谢jpierson提供的链接,看起来很有趣。 - akjoshi
1
如果所有的处理操作都被同一个未重写的类包装起来,并对其进行Interlocked.Exchange以确保没有冗余的处理操作,那么基类的IsDisposed标志就可以为派生类提供帮助。然而,根据Microsoft建议的实现方式,基层的IsDisposed标志实际上不能被子类使用。 - supercat

0

1) 不需要重复

2) 实现终结器将有助于处理未显式处理的项目。但不能保证。这是一个好习惯。


0

仅在对象保存有关需要清除的内容的信息,并且此信息以某种形式存储于其他需要清理的对象的对象引用之外(例如,作为Int32存储的文件句柄)时,才实现终结器。如果类实现了终结器,则不应保留对未进行清理的任何其他对象的强对象引用。 如果它将持有其他引用,则负责清理的部分应拆分为其自己的具有终结器的对象,并且主对象应保持对其的引用。然后,主对象不应具有终结器。

仅当基类的目的是支持终结器时,派生类才应该具有终结器。如果类的目的并不围绕终结器,那么允许派生类添加终结器没有太大意义,因为几乎可以肯定派生类不应该添加终结器(即使它们需要添加非托管资源,也应将资源放入自己的类中并只保留对其的引用)。


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