为什么Dispose()方法应该是非虚拟的?

38

我刚接触C#,如果这是一个显而易见的问题,请见谅。

MSDN释放范例中,他们定义的Dispose方法是非虚拟的。为什么呢?这对我来说似乎很奇怪 - 我本来希望一个IDisposable的子类如果有自己的非托管资源,只需要覆盖Dispose并在自己方法的底部调用base.Dispose()。

谢谢!


可能是C# Finalize/Dispose模式的重复问题。 - Dour High Arch
1
@Dour:实际上,那个问题(以及它的答案)并没有解决接口中一个方法缺少“virtual”关键字的问题。 - Robert Harvey
@Robert,问题不在于接口中的方法缺少虚拟关键字。重载的Dispose(bool)方法缺失了,而这个方法需要虚拟关键字。另一方面,在接口中放置一个方法签名几乎强制该方法为公共方法(或者当我试图违反它时似乎是这样),而在完整的Finalize/Dispose模式中,重载的Dispose(bool)方法是受保护的。 - Cylon Cat
9个回答

19

典型用法是Dispose()方法被重载,其中有一个公共的非虚拟Dispose()方法和一个虚拟的受保护的Dispose(bool)方法。公共的Dispose()方法调用Dispose(true),而子类可以使用这个受保护的虚拟方法来释放它们自己的资源,并为父类调用base.Dispose(true)。

如果拥有公共Dispose()方法的类也实现了finalizer,则finalizer将调用Dispose(false),表示在垃圾回收期间调用了受保护的Dispose(bool)方法。

如果存在finalizer,则公共Dispose()方法还负责调用GC.SuppressFinalize()以确保finalizer不再活动,并且永远不会被调用。这允许垃圾收集器正常处理该类。具有活动finalizer的类通常只在经过gen0、gen1和gen2清理后作为最后的手段被回收。


6
您的描述非常精确。不过,public Dispose() 方法应在声明类型未被封闭时__始终__调用GC.SuppressFinalize() - Steven
3
@Cylon: [第1/3部分] 我同意任何具有终结器的类都应该具有 Dispose 方法。然而,这并不意味着终结器和 Dispose 应该在同一个类中。这个观点不成立,因为你并不总是设计这两个类(想一想框架设计者)。例如,看一下 System.IO.Stream。它实现了 IDisposable 接口,但没有终结器。然而,FileStream 确实实现了终结器。Stream 调用 SuppressFinalize,即使它自己没有实现终结器。 - Steven
3
@Cylon:【第2/3部分】当Stream无法时,FileStream必须在其Dispose(bool)方法中调用SuppressFinalize,这并不是很糟糕,但这不符合“处理模式”。更糟糕的是,在Stream类上实现一个空的终结器,因为这会增加开发人员忘记处理对象时对象保留在堆上的风险。谈到糟糕的设计:System.ComponentModel.Component实际上有一个空的终结器,当实例没有得到适当处理时,这会引起各种问题(我曾见过因此而抛出OOM)。 - Steven
4
@Cylon: 【第三部分/共三部分】最后说明一下,我提到了“sealed”,因为只有当你的类是sealed时,你才能确保没有人会继承你的类并添加finalizer。希望这能澄清问题。 - Steven
3
值得注意的是,在框架设计师中,密封类是规则而不是例外,因为预先评估用户在派生类中使用(可能会破坏)类的不同方式可能很困难。因此,你无需特别理由来密封一个类。参见http://blogs.msdn.com/b/ericlippert/archive/2004/01/22/61803.aspx - Robert Harvey
显示剩余7条评论

13

这绝对不是一件显而易见的事情。选择这种模式的原因是因为它在以下情况下表现良好:

  • 没有 finalizer 的类。
  • 有 finalizer 的类。
  • 可以从中继承的类。

虚拟的 Dispose() 方法可以在不需要终结的类中起作用,但在需要终结的类中却不太适用,因为这些类型通常需要两种类型的清理。即:受控清理和非受控清理。出于这个原因,该模式引入了 Dispose(bool) 方法。它可以防止清理代码的重复(其他答案中缺少此点),因为 Dispose() 方法通常会同时清理受控和非受控资源,而终结器只能清理非受控资源。


2
我认为这是对实际问题更直接的答案:“为什么Dispose()应该是非虚拟的?”进一步的澄清可能是解释,关于“两种清理类型”,即使基类没有需要终结(因此需要自己的Dispose(bool)),未来的派生类可能也需要。如果发生这种情况,并且基类没有提供virtual Dispose(bool),那么派生类实现无法调用base.Dispose(bool),因此无法正确处理基类的处理。 - bob
我认为 Finalize 不是主要问题。更重要的因素是确保派生类可以以相同的方式添加 Dispose 逻辑,无论实现 IDisposable 的基本级别类是否公开公共 Dispose 方法(而不是显式实现 Dispose)。如果 Foo1 显式实现 Dispose,并且 Foo2 继承 Foo1 但也具有公共 Dispose 方法,则继承 Foo2Foo3 应该覆盖哪个方法?说所有功能都应在受保护的虚拟方法中,并且所有公共方法都应链接到该方法... - supercat
消除了歧义。 - supercat

5
虽然接口中的方法通常不是“虚拟”的,但它们仍然可以在继承它们的类中实现。这似乎是内置于 C# 语言中的一种便利,允许创建接口方法而无需使用“virtual”关键字,并且实现方法而无需使用“override”关键字。
因此,尽管 IDisposable 接口包含一个 Dispose() 方法,但它并没有在前面加上 virtual 关键字,也不需要在继承类中使用 override 关键字来实现它。
通常的 Dispose 模式是在自己的类中实现 Dispose,然后调用基类中的 Dispose,以便它可以释放所拥有的资源,依此类推。
一个类型的 Dispose 方法应该释放所有它拥有的资源。它还应该通过调用其父类型的 Dispose 方法来释放其基类型所拥有的所有资源。父类型的 Dispose 方法应该释放所有它拥有的资源,并依次调用其父类型的 Dispose 方法,通过基类型层次结构传播此模式。

http://msdn.microsoft.com/en-us/library/fs2xkftw.aspx


这引用了哪个文档? - Joel Etherton
3
这归结于一个事实,即您不希望依赖孩子来清理东西,因为他们可能不知道如何最好地清理父母的资源。 - Justin Niessner
@Joel:指的是原帖中链接的那个。 - Robert Harvey
这意味着它应该是虚拟的,对吗? - Grzenio
2
@Grzenio 不,如果子类重写了Dispose方法,那么基类的Dispose方法将不再可访问,因此父类的资源将永远不会被释放。 - Mark LeMoine

4
Dispose方法不应该是虚拟的,因为它不是实现可处理的模式的扩展点。这意味着在层次结构中的基本可处理类将创建顶级策略(算法)进行处理,并将详细信息委托给其他方法(Dispose(bool))。这个顶级策略是稳定的,不应该被子类覆盖。如果允许子类覆盖它,它们可能不会调用算法的所有必要部分,这可能会使对象处于不一致状态。
这类似于 模板方法模式,其中高级方法实现算法框架并将详细信息委托给其他可重写方法。
顺便说一下,我更喜欢另一种针对这个特定模式的高级策略(仍然使用非虚拟的Dispose)。

3
无论“普通”调用是否为直接或虚拟调用,通过接口进行的调用始终是虚拟的。如果实际执行处理处置工作的方法除了通过接口调用外不是虚拟的,则每次类想要处置自身时,它都必须确保将其自身引用强制转换为iDisposable并调用它。
在模板代码中,期望父类和子类中的非虚拟Dispose函数始终相同[仅调用Dispose(True)],因此从不需要覆盖它。所有工作都在虚拟Dispose(Boolean)中完成。
坦白地说,在没有理由期望后代类直接持有非托管资源的情况下,我认为在使用处置模式有点愚蠢。在.net早期,类通常需要直接持有非托管资源,但今天在大多数情况下,我看到没有任何损失,只需直接实现Dispose()即可。如果将来的后代类需要使用非托管资源,则可以并且通常应该将这些资源包装在它们自己的Finalizable对象中。
另一方面,对于某些类型的方法,具有非虚拟基类方法的优点可能会更好,其工作是链接到受保护的虚拟方法,并且使用名为Dispose(bool)的虚拟方法实际上并不比使用名为VirtDispose()的虚拟方法更糟糕,即使提供的参数相当无用。例如,在某些情况下,可能需要通过由基类对象拥有的锁来保护对象上的所有操作。在调用虚拟方法之前,使非虚拟基类Dispose获取锁将使所有基类不必再担心锁。

2
样本的Dispose()方法为什么不是虚拟的呢?因为在该示例中,它完全接管了整个过程,并留下了子类覆盖的虚拟Dispose(bool disposing)方法。您会注意到,在示例中,它存储了一个布尔字段,以确保Dispose逻辑不会被调用两次(可能一次来自IDisposable,一次来自析构函数)。覆盖提供的虚拟方法的子类无需担心这个细节。这就是为什么示例中的主Dispose方法不是虚拟的原因。

如果示例还有一个调用Dispose(bool)的终结器,那么它的示例将更加明显。 - Joel Rondeau
很遗憾,Disposed字段被用于派生类无用的方式。更好的方法是使用一个整数DisposalState字段,在非虚拟Dispose方法中可以使用Interlocked.Exchange进行测试和设置。 - supercat

1

我对dispose模式这里有一个相当详细的解释。基本上,您提供了一个protected方法来覆盖,以便更可靠地处理非托管资源。


0
如果基类在Dispose()时需要清理资源,则具有被继承类重写的虚拟Dispose方法会防止这些资源被释放,除非继承类明确调用基类的Dispose方法。更好的实现方式是让每个派生类实现IDisposable

如果你正在覆盖虚拟方法并且没有调用基本实现,那么你已经走上了一条艰难的道路。 - Kirk Woll
这是正确的,这就是为什么我建议为每个派生类实现IDisposable。使Dispose虚拟化是可能的,但是派生类必须调用基类的Dispose方法。 - SwDevMan81

0
另一个不那么明显的原因是避免为派生类抑制CA1816警告的需要。这些警告看起来像这样。
[CA1816] Change Dispose() to call GC.SuppressFinalize(object). This will prevent derived types that introduce a finalizer from needing to re-implement 'IDisposable' to call it.

这里是一个例子

class Base : IDisposable
{
    public virtual void Dispose()
    {
        ...

        GC.SuppressFinalize(this);
    } 
}

public class Derived : Base 
{
    public override void Dispose() // <- still warns for CA1816
    {
        base.Dispose();

        ...
    }
}

你可以通过采用推荐的Dispose模式来解决这个问题。

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