【问题标题】:Why should Dispose() be non-virtual?为什么 Dispose() 应该是非虚拟的?
【发布时间】:2010-09-01 15:04:31
【问题描述】:

我是 C# 的新手,如果这是一个明显的问题,我深表歉意。

MSDN Dispose example中,他们定义的Dispose方法是非虚拟的。这是为什么?这对我来说似乎很奇怪 - 我希望拥有自己的非托管资源的 IDisposable 的子类只会覆盖 Dispose 并在自己的方法底部调用 base.Dispose()。

谢谢!

【问题讨论】:

  • @Dour:实际上,这个问题(及其答案)并没有解决interface 中缺少virtual 关键字的方法问题。
  • @Robert,不是接口中的方法缺少 virtual 关键字。 overloaded Dispose(bool) 丢失了,这就是需要 virtual 关键字的那个。另一方面,将方法签名放在接口中几乎会强制方法公开(或者当我试图与之相反时似乎如此),并且重载 Dispose(bool) 在完整的 Finalize 中受到保护/Dispose 模式。

标签: c# .net dispose idisposable


【解决方案1】:

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

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

如果有终结器,那么公共的 Dispose() 方法也负责调用 GC.SuppressFinalize() 以确保终结器不再处于活动状态,并且永远不会被调用。这允许垃圾收集器正常处理类。具有活动终结器的类通常仅在 gen0、gen1 和 gen2 清理之后作为最后手段被收集。

【讨论】:

  • 您的描述非常准确。不过需要注意的是,public Dispose() 方法应该始终在声明类型密封时调用GC.SuppressFinalize()
  • @Cylon:[第 1/3 部分] 我同意任何带有终结器的类都应该有一个 Dispose 方法。然而,这并不意味着终结器和Dispose 应该在同一个类中。这不成立,因为您并不总是设计两个类(想想框架设计者)。以System.IO.Stream 为例。它实现了IDisposable,但没有终结器。 FileStream 然而,实际上确实实现了一个终结器。 Stream 调用 SuppressFinalize,即使它本身没有实现终结器。
  • @Cylon: [第 2/3 部分] 当 Stream 没有时,FileStream 不得不在它的 Dispose(bool) 方法中调用 SuppressFinalize,这还不错,但这不遵循“处置模式”。更糟糕的是在 Stream 类上实现一个空的终结器,因为当开发人员忘记处置他们的对象时,这会增加保持在堆上的对象的变化。谈论糟糕的设计:System.ComponentModel.Component 实际上有一个空的终结器,当实例没有正确处理时,这会导致各种麻烦(我已经看到 OOM 被抛出)。
  • @Cylon:[第 3/3 部分] 最后一点,我提到了“密封”,因为只有当您的类被密封时,您才能确定没有人从您的类继承并因此添加终结器。我希望这可以解决问题。
  • @Steven:值得注意的是,在框架设计者中,密封类是规则而不是例外,因为很难提前评估用户可能使用的所有不同方式(并可能破坏)派生类中的类。因此,你不需要特殊的理由来封课。见blogs.msdn.com/b/ericlippert/archive/2004/01/22/61803.aspx
【解决方案2】:

这当然不是一个显而易见的问题。之所以选择此模式,是因为它在以下场景中效果很好:

  • 没有终结器的类。
  • 确实有终结器的类。
  • 可以继承的类。

虽然虚拟Dispose() 方法可以在类不需要终结的情况下工作,但在您确实需要终结的情况下它不能很好地工作,因为这些类型通常需要两种类型的清理。即:托管清理和非托管清理。因此,模式中引入了Dispose(bool) 方法。它可以防止清理代码的重复(其他答案中缺少这一点),因为Dispose() 方法通常会清理托管和非托管资源,而终结器只能清理非托管资源。

【讨论】:

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

虽然接口中的方法不是通常意义上的“虚拟”方法,但它们仍然可以在继承它们的类中实现。这显然是 C# 语言内置的一种便利,允许创建接口方法而不需要 virtual 关键字,并实现方法而不需要 override 关键字。

因此,IDisposable接口虽然包含Dispose()方法,但它前面没有virtual关键字,也不必在继承类中使用override关键字来实现.

通常的Dispose模式是在自己的类中实现Dispose,然后在基类中调用Dispose,这样它就可以释放自己拥有的资源,以此类推。

一个类型的 Dispose 方法应该释放 它拥有的所有资源。它 还应该释放所有资源 通过调用其基类型拥有 父类型的 Dispose 方法。这 父类型的 Dispose 方法应该 释放它拥有的所有资源并 依次调用其父类型的 Dispose 方法,传播这种模式 通过基类型的层次结构。

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

【讨论】:

  • 这引用了什么文件?
  • +1 归结为这样一个事实,即当孩子可能不知道如何最好地清理父母的资源时,您不想依赖孩子来清理。
  • @Joel:OP 链接到的那个。
  • 这意味着它应该是虚拟的,不是吗?
  • @Grzenio 不,如果子类重写 Dispose,则基类的 Dispose 方法将不再可访问,因此父类的资源将永远不会被释放。
【解决方案4】:

Dispose 方法不应该是虚拟的,因为它不是模式实现一次性的扩展点。这意味着层次结构中的基本一次性类将为 dispose 创建顶级策略(算法)并将详细信息委托给另一个方法(Dispose(bool))。 此顶级策略是稳定的,不应被子类覆盖。如果您允许子类覆盖它,它们可能不会调用算法的所有必要部分,这可能会使对象留在不一致的状态。

这类似于template method pattern,其中高级方法实现算法骨架并将细节委托给其他可覆盖的方法。

作为旁注,我更喜欢 another 用于此特定模式的高级策略(仍然使用非虚拟 Dispose)。

【讨论】:

    【解决方案5】:

    无论“正常”调用是直接调用还是虚拟调用,通过接口进行的调用始终是虚拟的。如果实际执行处置工作的方法不是虚拟的,除非通过接口调用,那么任何时候该类想要处置自己,都必须确保将其自引用转换为 iDisposable 并调用它。

    在模板代码中,非虚拟的 Dispose 函数在父子节点中应该总是相同的[只需调用 Dispose(True)],因此永远不需要重写它。所有的工作都在虚拟的 Dispose(Boolean) 中完成。

    坦率地说,我认为在没有理由期望后代类直接持有非托管资源的情况下,使用 Dispose 模式有点愚蠢。在 .net 的早期,通常需要类直接持有非托管资源,但今天在大多数情况下,我发现直接实现 Dispose() 是零损失。如果未来的后代类需要使用非托管资源,它可以并且通常应该将这些资源包装在它们自己的 Finalizable 对象中。

    另一方面,对于某些类型的方法,拥有一个非虚拟基类方法,其工作是链接到受保护的虚拟方法,并且将虚拟方法称为 Dispose(bool) 确实是没有好处的比VirtDispose() 更糟糕,即使提供的参数相当无用。例如,在某些情况下,对象上的所有操作可能需要由基类对象拥有的锁来保护。让非虚基类Dispose 在调用虚方法之前获取锁将使所有基类不必担心自己的锁。

    【讨论】:

      【解决方案6】:

      示例的 Dispose() 方法是非虚拟的原因是因为它们接管了该示例中的整个过程,并留下具有虚拟 Dispose(bool disposing) 方法的子类进行覆盖。您会注意到,在示例中,它存储了一个布尔字段以确保 Dispose 逻辑不会被调用两次(可能一次来自 IDisposable,一次来自析构函数)。覆盖提供的虚方法的子类不必担心这种细微差别。这就是示例中的主要 Dispose 方法是非虚拟方法的原因。

      【讨论】:

      • 如果它还有一个调用 Dispose(bool) 的终结器,这个例子会更明显。
      • 太糟糕了,Disposed 字段的使用方式对派生类毫无用处。最好是一个整数 DisposalState 字段,可以在非虚拟 Dispose 方法中使用 Interlocked.Exchange 进行测试和设置。
      【解决方案7】:

      我对 dispose 模式 here 有一个非常详细的解释。本质上,您提供了一个 protected 方法来覆盖,而不是对非托管资源更健壮。

      【讨论】:

        【解决方案8】:

        如果基类有资源需要在Dispose() 时间清理,那么拥有一个被继承类覆盖的虚拟Dispose 方法会阻止这些资源被释放,除非继承类专门调用 基地的Dispose 方法。实现它的更好方法是让每个派生类都实现IDisposable

        【讨论】:

        • 嗯...如果您要覆盖虚拟方法而不调用基本实现,那么您已经处于一个受伤的世界。
        • 确实如此,这就是为什么我建议为每个派生类实现IDisposable。可以将Dispose 设为虚拟,但派生类必须调用基类Dispose 方法
        【解决方案9】:

        另一个不那么明显的原因是避免需要抑制派生类的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 模式即可解决此问题。

        【讨论】:

          猜你喜欢
          • 2014-04-06
          • 1970-01-01
          • 2014-03-15
          • 2016-10-18
          • 2011-03-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-04-13
          相关资源
          最近更新 更多