65.9K
CodeProject 正在变化。 阅读更多。
Home

再看 IDisposable

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (44投票s)

2003年8月30日

5分钟阅读

viewsIcon

285798

downloadIcon

1490

讨论 Dispose 方法,如何使用它,何时使用它,以及使用它时遇到的问题。

引言

这是又一篇关于接口类 IDisposable 用法的文章。  本质上,您将在这里看到的代码与 MSDN 上的代码或这两篇优秀文章中的代码没有区别:

Chris Maunder 的 .NET 中的垃圾回收
https://codeproject.org.cn/managedcpp/garbage_collection.asp

Eddie Velasquez 的 C# 类实现通用指南
https://codeproject.org.cn/csharp/csharpclassimp.asp

不同之处在于,本文例证了以下方面的功能:

  • 析构函数
  • Dispose 方法
  • 内存分配
  • 垃圾回收问题

换句话说,有很多文章展示了 如何 实现 IDisposable,但很少有文章演示 为什么 要实现 IDisposable

如何实现 IDisposable

下面代码的要点是:

  • 实现 IDisposable 接口的 Dispose 方法
  • 只 Dispose 资源一次
  • 实现类需要一个析构函数
  • 如果资源已被手动 Dispose,则阻止 GC Disposal 资源
  • 跟踪 GC 是在 Disposal 对象,而不是应用程序特意请求 Disposal 对象。  这涉及到对象管理资源的处理方式。

这是一张流程图

请注意两点:

  1. 如果对对象使用了 Dispose,它将阻止调用析构函数,并手动释放托管和非托管资源。
  2. 如果调用了析构函数,它只会释放非托管资源。  任何托管资源将被解除引用,并(可能)被回收。

这有两个问题,我稍后会回来讨论。

  1. 使用 Dispose 并不能阻止您继续与对象交互!
  2. 托管资源可能已被 Dispose,但仍被代码中的某个地方引用!

下面是一个实现 IDisposable 的示例类,它管理一个 Image 对象,并已进行仪器化以说明类的运行情况。

public class ClassBeingTested : IDisposable
{
   private bool disposed=false;
   private Image img=null;
   
   public Image Image
   {
      get {return img;}
   }

   // the constructor
   public ClassBeingTested()
   {
      Trace.WriteLine("ClassBeingTested: Constructor");
   }

   // the destructor
   ~ClassBeingTested()
   {
      Trace.WriteLine("ClassBeingTested: Destructor");
      // call Dispose with false.  Since we're in the
      // destructor call, the managed resources will be
      // disposed of anyways.
      Dispose(false);
   }

   public void Dispose()
   {
      Trace.WriteLine("ClassBeingTested: Dispose");
      // dispose of the managed and unmanaged resources
      Dispose(true);

      // tell the GC that the Finalize process no longer needs
      // to be run for this object.
      GC.SuppressFinalize(this);
   }

   protected virtual void Dispose(bool disposeManagedResources)
   {
      // process only if mananged and unmanaged resources have
      // not been disposed of.
      if (!this.disposed)
      {
         Trace.WriteLine("ClassBeingTested: Resources not disposed");
         if (disposeManagedResources)
         {
            Trace.WriteLine("ClassBeingTested: Disposing managed resources");
            // dispose managed resources
            if (img != null)
            {
               img.Dispose();
               img=null;
            }
         }
         // dispose unmanaged resources
         Trace.WriteLine("ClassBeingTested: Disposing unmanaged resouces");
         disposed=true;
      }
      else
      {
         Trace.WriteLine("ClassBeingTested: Resources already disposed");
      }
   }

   // loading an image
   public void LoadImage(string file)
   {
      Trace.WriteLine("ClassBeingTested: LoadImage");
      img=Image.FromFile(file);
   }
}

为什么实现 IDisposable?

我们将此类放入一个简单的 GUI 测试夹具中,如下所示:

我们将使用两个简单的工具来监视正在发生的事情:

  • DebugView 用于观察执行: http://www.sysinternals.com/ntw2k/freeware/debugview.shtml
  • 任务管理器性能视图用于内存利用率。

测试非常简单,涉及多次加载一个 3MB 的图像文件,并可以选择手动 Dispose 对象。

private void Create(int n)
{
   for (int i=0; i<n; i++)
   {
      ClassBeingTested cbt=new ClassBeingTested();
      cbt.LoadImage("fish.jpg");
      if (ckDisposeOption.Checked)
      {
         cbt.Dispose();
      }
   }
}

顺便说一下,那个毫不知情的鱼是一条 Unicorn Fish,摄于加州圣地亚哥海洋世界。

观察我创建 10 条鱼时会发生什么。

十条鱼占用了 140MB(这很奇怪,因为这条鱼只是一个 3MB 的文件,所以您会认为最多只消耗 30MB,但我们不会深入探讨 THAT)。

此外,请注意,对象上的析构函数从未被调用。

如果我们创建 25 条鱼,然后又创建 10 条鱼,请注意由于大量的磁盘交换,捕捞鱼所需的时间会发生什么变化。

现在平均加载一条鱼需要两秒钟!  GC 什么时候开始回收垃圾?  没有!  相反,如果我们一用完类就 Dispose 它(在我们的测试案例中是立即),就不会出现内存占用和性能下降。  所以,委婉地说,考虑一个类是否需要实现 IDispose 接口,以及是否要手动 Dispose 对象,是非常重要的。

幕后

让我们创建一个鱼,然后强制 GC 回收它。  生成的跟踪如下:

在此情况下,请注意,调用了析构函数,并且托管资源没有被手动 Dispose。

现在,让我们改为创建一个带 Dispose 标志的鱼,然后强制 GC 回收它。  生成的跟踪如下:

在此情况下,请注意,托管和非托管资源都已被 Dispose,并且调用析构函数已被抑制。

问题

如上所述,即使对象已被 Dispose,也没有什么能阻止您继续使用该对象以及您可能获得的任何对其管理对象的引用,如下面的代码所示:

private void btnRefTest_Click(object sender, System.EventArgs e)
{
   ClassBeingTested cbt=new ClassBeingTested();
   cbt.LoadImage("fish.jpg");
   Image img=cbt.Image;
   cbt.Dispose();
   Trace.WriteLine("Image size=("+img.Width.ToString()+", "+img.Height.ToString()+")");
}

当然,结果是:

解决方案

理想情况下,当

  • Dispose 被调用且托管对象仍在其他地方被引用时
  • 在对象被 Dispose 后调用方法和访问器

应该断言或抛出异常。  不幸的是(据我所知),无法访问对象的引用计数,因此很难确定托管对象是否仍在其他地方被引用。  除了要求应用程序“释放”引用外,最佳解决方案是根本不允许另一个对象获取对内部托管对象的引用。  换句话说,代码:

public Image Image
{
   get {return img;}
}

不应该在“管理”类中允许。  相反,管理类应实现其他类所需的所有必要支持函数,实现对托管对象的封装。  使用这种方法,应用程序可以在 disposed 标志为 true 时抛出异常——表明对象在技术上已被 Dispose 后仍在使用。

结论 - 单元测试

我之所以进行这么一番周折,是因为我想展示单元测试的不足之处。  例如,假设我上面描述的测试类没有实现 IDisposable。  在这里,我们有一个绝佳的例子,说明了对类及其函数的单个测试将如何出色地成功,从而产生一种错觉,即使用该类的程序一切正常。  但并非一切正常,因为该类没有为应用程序提供 Dispose 其托管资源的方法,最终导致整个计算机系统因内存碎片和磁盘交换而陷入停滞。

这并不意味着单元测试不好。  但它确实说明了它远非全貌,像 NUnit 这样的单元测试应用程序可以有很大的发展,以帮助程序员自动化更复杂的单元测试形式。

而这,我的朋友们,将是下一篇文章的主题。

下载演示项目

我故意省略了“fish.jpg”,因为它有 3MB 大。  如果您想玩代码,请编辑代码并使用您自己的 JPG。

© . All rights reserved.