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

IDisposable 的基础知识(以及一些最佳实践)

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.95/5 (17投票s)

2003年9月1日

12分钟阅读

viewsIcon

107229

面向对 OOP (面向对象编程) 了解不多的初学者 .NET 开发者。本文讨论了 IDisposable 的基础知识,并试图展示其内在的简单性。

引言

对于一些初学者 .NET 程序员来说,IDisposable 接口的某些方面令人困惑。以下文章讨论了 IDisposable 的基础知识,并试图展示其内在的简单性。此外,还提出了一种直接的逻辑,初学者可以从中开始。

背景 (可跳过)

在工作中,我们正开始向 .NET 过渡。一切进展顺利,但并非一帆风顺。不,问题不在于 .NET。问题是如何将 VB 6 和 PowerBuilder 的程序员转移到这个强大且经常优雅的平台。管理层采取的策略之一是挑选出我们现在称之为“导师”的个人(我曾有疑虑,但是的,我就是“被选中者”之一)。这样的人,希望至少是技术娴熟的程序员,能够学习 .NET 并帮助他人完成转型。现在,导师们必须接受额外的 .NET 培训课程。在其中一门课程中,发生了一件奇怪的事情。老师正在讲授垃圾回收以及相关的 IDisposable 接口。当他讲完时,有很多问题。我个人不喜欢老师讲授材料的方式。所有的细节都涵盖了,但新 .NET 学生最需要的东西,“给我说重点,我什么时候用这个东西”——却缺失了。总之,我从一些学生那里感受到的是困惑。奇怪的是,在接下来的“导师”课程中,同一个主题以相同的方式讲解,结果也大致相同。

但让我先退一步。我如何学习一门新的语言/平台?我买了很多书并阅读它们——我直接上手开始编写代码——基本上管理层知道让我一个人待着,我就会弄明白。我之所以提起这一点,是因为我也曾在这个 IDisposable 问题上停滞不前。我现在觉得我面临的真正问题是 .Net 是新的,而最好的信息来源(至少在数量上)是微软。无论你喜欢 MS 还是不喜欢,或者是一种爱恨交织的关系,我认为很少有人会不同意,在教学和文档方面,MS 经常做得不够。我现在用 .NET 编程已经一年了。那时,我解决这类问题的办法就是继续前进——在 .NET 学习了大约三个月后,我明白了。IDisposable 和那个 Dispose() 方法——它很简单。之所以写这篇文章,是因为可能还有其他人发现这个主题有些令人困惑。

运行时的角色

下面是一张图,概念性地描绘了一个特定的 .NET 应用程序及其与运行时的交互。读者应该知道,这张图是简化的(而且,虽然我对 Windows 内部机制有所了解,但我绝不是专家)。然而,对运行时角色,特别是其内存/资源管理的通识性了解,是正确使用 IDisposable 接口所必需的。这张图显示了一个名为“SomeAppOfMine”的 .NET 应用程序,该应用程序被构建然后运行。在此示例中,该应用程序使用 COM Interop 来创建和使用一个 COM 对象。

        ---------------------SomeAppOfMine - (App. Dev. Layer)
        |
        |
        | Build/Run
        |
        |
        |                                .NET Runtime
        v      ________________________________________________
        My IL  |                                              |
        |      | Stack Heap{ ... ObjX ... }<---Manages----GC  |
        |      |               ^  |                           |
        |      |_______________|__|___________________________|
        |                      |  |
        |                      |  |
        |______________________|  |
                                  |
                                  |                   Other Memory
                                  |      ______________
                                  |     |             |
             Request COM Obj.     |     |... ObjY ... |
                                  |     |______^______|
                                  |            |
                                  |            |Creates
         Windows OS layer         |     _______|          
              ____________________|____|_______________________
             |                    v    |                      |
             |               Some Win API                     |
             |                                                |
             |________________________________________________| 

上图中最重要的概念是三层架构。最上面是应用程序开发者的空间。这种设计允许推迟或忽略下层的许多细节。这种安排很有吸引力,因为开发者可以全神贯注于编写代码——至少通常是这种情况。然而,在某些情况下,这种架构并不能轻易解决问题(例如我们现在讨论的主题)。在这种情况下,高层次的视图可能会导致一些困惑,即使相关的低级细节很简单。正确使用 IDisposable 就属于这种情况。

中间是 .NET 运行时。这张图有意这样布局,以强调运行时“中间人”的角色。通过这种设计,应用程序的构建不是面向机器语言,而是面向 IL (中间语言)。有些人称之为虚拟机层。IL 代码是标准化的,因此运行时可以完全管理其执行。它也是一种通用代码,理论上可以(至少在理论上)在任何带有任何操作系统的计算机上执行(可能是 IT 历史上最伟大的解耦设计之一,只是 Java 和其他我听说的东西先做到了)。要理解如何正确使用 IDisposable,必须清楚地区分什么是托管的,什么是非托管的。就 IDisposable 而言,要问的问题是“上面的 IL 代码是托管的吗?”答案当然是肯定的——它在中间——它是托管的。

最底层是 Windows。要让 Windows 做任何事情,都必须在某个环节调用 Win API。这种方法的原因也很简单。例如,在 C 语言中,你可以将一个指针指向内核所在的内存区域并尝试覆盖它。这显然是件坏事(Visual Studio 崩溃,或者最糟糕的情况是“蓝屏死机”)。Windows 不允许这种请求,它坚持所有对计算机的使用都必须通过其自身的 API。在上图中,托管对象“ObjX”通过 COM Interop 请求 Windows 创建一个特定的 COM 对象。Windows 答应了并将 ObjY 加载到内存中。上述设计带来的问题是,运行时层和 Windows 层无法知道如何或何时进行适当的清理。 .NET 运行时只知道 COM 对象中公开的内容(如“COM 术语”中所说的其公开的接口)。Windows 知道 COM 对象使用的所有资源以及启动它的进程。但是 Windows,因为在架构中处于较低层,所以必须被告知何时移除 COM 对象(或等到应用程序关闭)。总之,就像上面一样,要问的重要问题是“这个 COM 对象是托管的吗?”当然答案是否定的。

例如,一个(为了说明而承认的极端例子)假设 ObjY 占用了大量内存和/或许多系统资源(文件通道、数据库连接、TCP 套接字,可能性列表很长)。然后,已完成 ObjX 的托管 IL 代码继续执行其他任务。ObjX 现在超出范围,并最终被垃圾回收器销毁。但是 IL 代码本身并未完成。或者另一种可能性是,用户切换到另一个应用程序,但让 .NET 应用程序保持运行。也许用户认为编写此 .NET 应用程序的开发人员很有知识,并且此操作不会损害(削弱?)计算机的性能。这将是一个完全错误的假设。从这个例子可以看出,.NET 必须允许我们在不再需要非托管资源时处理它们。这种情况可能很严重,即使开发人员必须编写几行代码并遵循一些“最佳实践”,这种要求显然也优于没有任何解决方案。事实证明,这正是 .NET 设计者的选择——他们的解决方案是 IDisposable

现在给出一些答案(终于)

IDisposable 只实现了一个方法——Dispose()。那么,初学者如何决定是调用 Dispose() 还是实现 IDisposable 接口呢?考虑到上述概念,现在可以提出几个简单的问题。初学者可能面临的大多数与 IDisposable 相关的情况现在将分为三类。现在将讨论这些类别。我还必须定义一个新术语(我自己的创造)。该术语是“有价值的代码”。有价值的代码可以是以下任何一项:

  1. 将要或可能投入生产的代码
  2. 将要或可能用于某些“有价值”任务的代码
  3. 将要或可能被重用的代码

等等...

“无价值的代码”是所有剩余的代码。例如,一个人可能只是为了学习目的而探索某个框架类的用法。或者,一个人可能试图让一个框架类以某种特定方式执行某个任务。也许只是为了看看它是否能做到,仅仅因为“这会很酷”。在以下三种涉及 IDisposable 接口的问题的案例中,前两种涉及被视为“有价值”的代码。第三种情况的代码则不是。

案例 #1(有价值)

正在使用一个实现了 IDisposable 的框架类(或一个自研的)。完成对结果对象的处理后,是否应该调用 Dispose()

答案——是。

讨论:这只是一行额外的代码,清理成本也会更低。在这种情况下,你几乎没有什么损失(可能——请参阅下面的限制部分),却能获得很多收益。如果这是最少成本的情况,即需要一个对象在其生命周期内占用少量资源,并且性能提升不会明显,那么为什么不让进程关闭来完成所有清理呢?我仍然建议初学者 .NET 程序员进行调用。最佳实践(不仅在我的公司,而且在编码界普遍如此)决定了这一点。对某些人来说,这可能有点严格,是的,这个问题有点哲学性。但在作者看来,最佳实践就是这样——你应该始终去做的事情——如果不是,也要有一个非常好的理由来打破规则。即使没有明显的好处,遵循最佳实践也是一种好方法。这样,当它真正重要时,你就能自动去做。

案例 #2(有价值)

正在编写一个外观层(此处选择该示例——它可以是任何被“使用”的类),它简化了框架类(或自研类)的使用。外观类是否应该实现 IDisposable 以将此功能暴露给所有用户?

答案——是。

讨论:是的,但只有一个微小的注意事项——外观类不是“进去-出来-我们完成”的类型(这在实际的、中低级代码中并不常见)。通常,必须告诉外观类它的用户何时完成。不实现 IDisposable 将隐藏一项急需的功能。或者用 OOP 的方式来说,有些东西不应该被封装,但却被封装了。那么,有人可能会问,什么是正确实现 IDisposable 的良好方法?一种典型技术是添加三个方法,即使只需要一个——如下所示。

 ________________
|                |
|public Dispose()|
|________________|
      |
      |
      | pass in "true"
      |
      |
______V__________________________
|                                |
| protected virtual Dispose(bool)|
| // do a proper clean up        |
|________________________________|
      ^
      |
      |
      | pass in "false"
      |
      |
 _____|_________________
|                       |
| protected ~Finalize() |
|_______________________|

下面将涵盖掌握上述方法所必需的关键概念。公共 Dispose() 方法是供类用户使用的。它不仅是一个代码声明,也是一个非常公开的宣告(呐喊?),告诉所有用户他们可能应该“完成后调用我”。finalize 方法则完全相反——只有 .NET 运行时应该调用它。protected virtual Dispose(bool) 方法起初看起来很微妙,但稍加思考,它的存在理由也就变得清晰了。它存在的原因有两个。首先,这种方法遵循了一个长期以来建立的最佳实践,即始终在一个地方完成某项特定任务,并且只在一个地方完成。但是 OOP 的引入带来了第二个动机。假设该类不是密封的,意味着它可以被继承。如果它可以被继承,那么虔诚的开发人员会调整类的设计,以实现良好的继承。会不会出现这样的情况,当被继承时,用户可能希望重写特定的销毁方法(对其进行变形)?由于 Dispose(bool) 被声明为这样,这个选项仍然存在。

案例 #3(无价值)

情况与上述两种情况之一相同,但代码是“无价值的”。是否应该调用 Dispose() (这就像案例 #1)或者是否应该实现 IDisposable (这就像案例 #2)?

答案——否。

讨论:这种情况显然是“谁在乎”。我只能想到一个例外。你想为了学习经验而编写一个 IDisposable 接口。

一些限制

作者希望明确指出,上述三个案例并不涵盖所有可能性或所有问题。它们被提出来是为了给 .NET 初学者提供一个起点。作者认为初学者将面临的大多数情况都在以上范围内得到了解决。但为了提高普遍意识,应该记住以下几点:

  • Finalize() 方法确实会产生一些开销。因此,即使最佳实践另有规定,你也可能会遇到需要考虑省略此调用的情况。
  • 如果你必须继承一个实现了 IDisposable 的类,那么还有一些额外的问题需要解决。初学者可以获取一个好的代码示例,或者向更有经验的人寻求帮助。

这个 IDisposable 在哪里?

这很容易错过,但有几个技巧可以提供帮助。问题在于,如果你查看 MSDN 中的类声明而找不到 IDisposable ,你不应该得出它不存在的结论(一句话里“不”出现了三次——我在这行干得太久了)。它可能,并且经常是,先前从一个存在的类或接口继承而来的(以“Component”这个普遍存在的类为例)。更好的方法是直接查找 Dispose() 方法。向下滚动到公共方法以“D”开头的区域,或者如果存在的话,那里应该有。如果应用程序中有大量的类,这种方法可能会非常繁琐,至少可以说。一个更好的方法是打开对象浏览器,逐个检查每个类。然而,对象浏览器只显示你已引用的类。如果正在检查一个类以备潜在使用,则第二个选择可能是最好的。

一个幸福的结局(结论)

初学者是否应该担心 IDisposable 接口是一个奇怪的问题,希望现在应该被视为完全不成立。你可以回到舒适的高层次——回到 Visual Studio。只需记住以上三个案例的答案,然后“去写代码”吧。

修订历史

  • 2003年9月2日
    • 根据 Blake Coverett 的评论删除了部分内容。
    • 进行了一些轻微的改写,并修正了一些拼写错误。
  • 2003年9月1日
    • 对文章进行了重写——收到的几条评论使得这次重写变得必要。
    • 最初的文章标题是“那个诡异的 IDisposable?”(现已删除),并以 SammyLovesC 的用户名提交(在我看来现在很俗气)。(在我看来现在很俗气)。
© . All rights reserved.