【问题标题】:How to better implement .NET IDisposable classes?如何更好地实现 .NET IDisposable 类?
【发布时间】:2011-10-26 17:07:06
【问题描述】:

如果这个问题有点过于开放,请提前原谅我,但我在这里看到过类似的语言讨论帖子,所以我想我会冒险。

无论如何,我已经阅读了几个关于正确实现IDisposable 类的 MSDN 帮助页面和其他各种博客。我觉得我理解的很好,但我不得不怀疑建议的类结构是否存在缺陷:

public class DisposableBase : IDisposable
{
    private bool mDisposed;

    ~DisposableBase()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!mDisposed)
        {
            if (disposing)
            {
                // Dispose managed resources
                mManagedObject.Dispose();
            }

            // Dispose unmanaged resources
            CloseHandle(mUnmanagedHandle);
            mUnmanagedHandle = IntPtr.Zero;

            mDisposed = true;
        }
    }
}

只要上述内容用作基类,您就可以依赖子类的实现者在必要时正确覆盖 Dispose(bool) 方法。简而言之,派生类必须确保它们从被覆盖的版本中调用基本的 Dispose(bool) 方法。否则,基类的非托管资源可能永远不会被释放,从而违背了 IDisposable 接口的主要目的。

我们都知道虚拟方法的好处,但在这种情况下,它们的设计似乎不足。事实上,我认为虚拟方法的这种特殊缺点在尝试设计可视组件和类似的基类/派生类结构时经常表现出来。

考虑以下更改,使用受保护的事件而不是受保护的虚拟方法:

public class DisposeEventArgs : EventArgs
{
    public bool Disposing { get; protected set; }

    public DisposeEventArgs(bool disposing)
    {
        Disposing = disposing;
    }
}

public class DisposableBase : IDisposable
{
    private bool mDisposed;

    protected event EventHandler<DisposeEventArgs> Disposing;

    ~DisposableBase()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    // This method is now private rather than protected virtual
    private void Dispose(bool disposing)
    {
        if (!mDisposed)
        {
            // Allow subclasses to react to disposing event
            AtDisposing(new DisposeEventArgs(disposing));

            if (disposing)
            {
                // Dispose managed resources
                mManagedObject.Dispose();
            }

            // Dispose unmanaged resources
            CloseHandle(mUnmanagedHandle);
            mUnmanagedHandle = IntPtr.Zero;

            mDisposed = true;
        }
    }

    private void AtDisposing(DisposeEventArgs args)
    {
        try
        {
            EventHandler<DisposeEventArgs> handler = Disposing;
            if (handler != null) handler(this, args);
        }
        catch
        {
        }
    }
}

通过这种设计,无论子类是否订阅 Disposing 事件,都会始终调用基类的 Dispose(bool) 方法。在这个修改后的设置中,我可以看到的最大缺陷是调用事件侦听器的时间没有预先确定的顺序。如果有多个继承级别,这可能会出现问题,例如SubclassA 的侦听器可能在其子 SubclassB 的侦听器之前触发。这个缺陷是否严重到足以使我修改后的设计无效?

这种设计困境让我希望有某种修饰符用于类似于virtual 的方法,但它可以确保始终调用基类的方法,即使子类覆盖了该函数。如果有更好的方法来实现这一点,我将非常感谢您的建议。

【问题讨论】:

  • 我认为要求派生类调用基本实现的方法的合同的一部分是合理的,特别是对于合同相当明确的 Dispose 等方法。更好的是,构建您的解决方案,以便密封 Disposable 类,您不必为整个 Dispose(bool) 混乱而烦恼。
  • 但这是一个真正的问题吗?拥有资源而不被密封(可密封)应该是罕见的。我宁愿使用单元测试。
  • 使用 SafeHandle 类之一,问题已解决。

标签: c# .net idisposable


【解决方案1】:

当你真的想使用像virtual 这样的继承机制时,你在这里使用了event。对于这样的场景,我想确保始终调用我的实现,但又想允许基类自定义,我使用以下模式

private void Dispose(bool disposing)
  if (mDisposed) { 
    return;
  }

  if (disposing) {
    mManagedObject.Dispose();
  }

  // Dispose unmanaged resources
  CloseHandle(mUnmanagedHandle);
  mUnmanagedHandle = IntPtr.Zero;
  mDisposed = true;

  DisposeCore(disposing);
}

protected virtual void DisposeCore(bool disposing) {
  // Do nothing by default
}

通过这种模式,我确保我的基类Dispose 实现将始终被调用。派生类不能通过简单地忘记调用基方法来阻止我。他们仍然可以通过覆盖 DisposeCore 来选择使用 dispose 模式,但他们不能破坏基类契约。

【讨论】:

  • 它会起作用,但是否值得偏离参考实现?
  • @HenkHolterman 恕我直言,是的。如果参考样本不能充分满足我的限制条件,我会根据需要进行偏离。
  • 我喜欢这个想法,而且它肯定比使用事件更少的代码。但是,如果有多个继承级别,这是否仍会遇到相同的原始问题(即孙子没有正确调用其父级的 DisposeCore 方法)?
  • @Jeremy 是的。这很烦人,因为您必须在名称方面发挥创意。但是,我通常会保持我的层次结构很短,所以这不是什么大问题。
  • 我认为对于我的具体问题,其他人在这里所说的是最正确的。如果可能,最好使用SafeHandle 类,或者至少只制作一个简单的密封类来包装非托管资源。无论如何,我很欣赏你的回答,尽管它仍然让我希望有一些其他语言继承机制,其中基类方法总是被调用、覆盖或不被覆盖。 =/
【解决方案2】:

派生类可以简单地重新实现IDisposable,从而防止调用您的dispose方法,因此您也无法确保。

我个人不会使用任何一种模式。我更喜欢在 SafeHandle 和类似机制上构建,而不是自己实现终结器。

【讨论】:

  • 您可以从问题中删除终结器部分,它仍然存在。
  • 我从未考虑过这一点,而且似乎使 IDisposable 类充满了问题。我刚刚偶然看到了这篇非常详细的帖子:IDisposable: What Your Mother Never Told You About Resource Deallocation。除了指出当前 IDisposable 问题的各种问题外,我认为他基本上提出了与您相同的建议。
  • @Jeremy SafeHandle 消除了析构函数,而不是您的继承问题。
  • @HenkHolterman 是的,我一开始不太明白你在说什么,但你说的完全正确。即使我的基类没有任何非托管资源,如果它有任何自己的 IDisposable 字段,它也应该实现 IDisposable 并且我们会遇到同样的问题,不是吗?
【解决方案3】:

考虑清楚地表明 Dispose 没有被调用,以便有人会捕获它。当然Debug.WriteLine只会在代码被定义为DEBUG编译器指令的情况下被调用。

public class DisposableBase : IDisposable
{
  private bool mDisposed;

  ~DisposableBase()
  {
      if (!mDisposed)
         System.Diagnostics.Debug.WriteLine ("Object not disposed: " + this + "(" + GetHashCode() + ")";
      Dispose(false);
  }

  public void Dispose()
  {
      Dispose(true);
      GC.SuppressFinalize(this);
  }

【讨论】:

  • 你能在终结器中安全地访问 Debug.WriteLine 吗? (即使没有,我已经读到您仍然可以在终结器中安全地写入 Console.Out,所以您的观点仍然成立)
  • 我是这么认为的,但您担心什么?终结器线程应该可以访问 Debug.WriteLine ()。
【解决方案4】:

你可以把它分解:

  • 只有非托管资源需要析构函数(终结器)。
  • 使用 Safehandle 可以将非托管资源转换为托管资源。
  • Ergo:你不需要析构函数。这使 Dispose 模式减半。

参考设计使用virtual void Dispose(bool) 来解决基类/派生类问题。这给派生类带来了调用base.Dispose(disposing) 的负担,这是您问题的核心。我使用两种方法:

1) 防止它。有了密封的基类,您就不必担心了。

sealed class Foo:IDisposable 
{ 
   void Dispose() { _member.Dispose(); } 
}

2) 检查它。就像@j-agent 的回答一样,但有条件。当性能可能成为问题时,您不希望在生产代码中使用终结器:

class Foo:IDisposable 
{ 
  void Dispose() { Dispose(true); }

  [Conditional("TEST")]  // or "DEBUG"
  ~Foo { throw new InvalidOperation("somebody forgot to Dispose") } 
}

【讨论】:

    【解决方案5】:

    无论是否有任何子类覆盖 Dispose() (可以通过覆盖或新建),都会调用析构函数,但是将调用你的析构函数(~DisposableBase())所以我敢打赌把你的清理逻辑放在那里可以作为一个很好的起点。

    这是一篇关于析构函数的有趣文章:http://www.c-sharpcorner.com/UploadFile/chandrahundigam/UnderstandingDestructors11192005021208AM/UnderstandingDestructors.aspx

    【讨论】:

    • 虽然语法(不幸的是)与 C++ 析构函数相同,但终结器方法不是析构函数;混淆术语会导致混淆。此外,如果 GC.SuppressFinalize 被调用,终结器将被调用。此外,运行时不保证会调用终结器,但实际上通常会调用。
    • 没错,@DanBryant,但我认为这里的问题归结为他正在编写的代码的使用,如果他正在编写内部库,那么这不是问题,因为您可以指定 没有人现在应该打电话给GC.SupressFinalize,如果它是一个公共图书馆,那么你无法保证用户将如何使用你的IDisposable实现。
    • @Dan Bryant:为了迂腐,C# 析构函数是一段代码,将从编译器生成的包装器中调用。语法类似于 C++ 析构函数,尽管语义完全不同。 C# 析构函数与 Finalize 的重写并不完全相同,因为前者包含一些(也许是愚蠢的)包装代码。如果允许重写 Finalize(),我建议程序员这样做并假装 C# 析构函数不存在,因为它们不会增加可移植性或表现力。尽管如此,它们仍然存在,而且“析构函数”是唯一准确的术语。
    • @Dan Bryant:当然,就终结器的大多数情况而言,C# 析构函数也是如此,人们可以(并且应该)在大多数可能的地方使用术语“终结器”使用术语“析构函数”。不过,终结器和 C# 析构函数之间存在一些细微差别,因此这两个术语都必须是词典的一部分。
    猜你喜欢
    • 1970-01-01
    • 2011-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-23
    • 2013-10-29
    相关资源
    最近更新 更多