【问题标题】:Should "Dispose" only be used for types containing unmanaged resources?“Dispose”是否应该只用于包含非托管资源的类型?
【发布时间】:2012-05-06 04:22:20
【问题描述】:

我最近和一位同事讨论了Dispose 的值以及实现IDisposable 的类型。

我认为对于应该尽快清理的类型实现IDisposable 是有价值的,即使没有要清理的非托管资源

我的同事想法不同;如果您没有任何非托管资源,则无需实施IDisposable,因为您的类型最终将被垃圾回收。

我的论点是,如果您有一个想要尽快关闭的 ADO.NET 连接,那么实现 IDisposableusing new MyThingWithAConnection() 将是有意义的。我的同事回答说,ADO.NET 连接实际上是一个非托管资源。我对他的回复是 一切最终都是非托管资源

我知道recommended disposable pattern,您如果调用Dispose,则释放托管和非托管资源,但如果通过终结器/析构函数调用,则仅释放非托管资源 (不久前还写了一篇关于如何alert consumers of improper use of your IDisposable types的博客)

所以,我的问题是,如果你有一个不包含非托管资源的类型,是否值得实现IDisposable

【问题讨论】:

  • 正如您正确指出的,ADO 连接非托管资源。
  • @KonradRudolph - 不。连接称为托管资源。它包含(拥有)非托管资源,尽管可能间接通过 SafeHandle。
  • @Henk 这就是我的意思——我应该更仔细地措辞,但在问题中它已经以正确的方式措辞了。
  • 在非托管资源之外,我唯一需要IDisposable 的时候是我需要确保正确取消订阅事件以便可以对类进行垃圾收集。但这确实是语言的失败:事件真的 REALLY 需要是弱引用,但事实并非如此。

标签: c# garbage-collection idisposable finalizer finalization


【解决方案1】:

IDisposable 有不同的有效用途。一个简单的例子是持有一个打开的文件,当您不再需要它时,您需要在某个时刻将其关闭。当然,您可以提供一个方法Close,但是将它放在Dispose 中并使用using (var f = new MyFile(path)) { /*process it*/ } 之类的模式会更安全。

一个更流行的例子是持有一些其他的IDisposable 资源,这通常意味着您需要提供自己的Dispose 才能处置它们。

一般来说,只要你想对任何东西进行确定性销毁,你就需要实现IDisposable

我和你的不同之处在于,我在某些资源需要确定性销毁/释放时立即实施IDisposable,而不是尽快。在这种情况下,依靠垃圾收集不是一种选择(与您同事的说法相反),因为它发生在不可预知的时刻,实际上可能根本不会发生!

任何资源在掩护下不受管理的事实并不意味着什么:开发人员应该考虑“何时以及如何处置该对象是正确的”而不是“它如何在掩护下工作”。无论如何,底层实现可能会随着时间而改变。

事实上,C# 和 C++ 之间的主要区别之一是没有默认确定性销毁。 IDisposable 弥补了差距:您可以订购确定性销毁(尽管您无法确保客户端正在调用它;与 C++ 中的相同方式,您无法确定客户端在对象上调用 delete)。


小补充:确定性释放资源和尽快释放资源之间实际上有什么区别?实际上,这些是不同的(尽管不是完全正交的)概念。

如果要确定地释放资源,这意味着客户端代码应该有可能说“现在,我想释放这个资源”。这实际上可能不是释放资源的可能的最早时刻:持有资源的对象可能已经从资源中获得了它所需的一切,因此它可能已经释放了资源。另一方面,即使在对象的Dispose 运行后,对象也可能选择保留(通常是非托管的)资源,仅在终结器中清理它(如果持有资源太长时间不会产生任何问题)。

所以,为了尽快释放资源,严格来说,Dispose 是没有必要的:对象可以在意识到自己不需要任何资源时立即释放资源更多的。 Dispose 然而作为一个有用的提示,对象本身不再需要,所以如果合适的话,资源可能会在那个时候被释放


还有一个必要的补充:需要确定性释放的不仅是非托管资源!这似乎是这个问题的答案中意见分歧的关键点之一。一个人可以有纯粹的想象结构,这可能需要确定性地释放。

例如:访问某些共享结构的权利(想想RW-lock)、巨大的内存块(假设您正在手动管理程序的某些内存)、使用其他程序的许可(假设您是不允许同时运行超过 X 个程序的副本)等。这里要释放的对象不是非托管资源,而是 权利 做/使用某些东西,这纯粹是您的程序逻辑的内部构造。


小补充:这里是[ab]使用IDisposable的简洁示例的小列表:http://www.introtorx.com/Content/v1.0.10621.0/03_LifetimeManagement.html#IDisposable

【讨论】:

  • 在文件和数据库连接等非托管资源之外,您何时需要进行确定性销毁? (还有一点小小的抱怨 - 对象本身没有被确定性破坏,只有它使用的资源......并且只有当这些资源不受管理时)
  • @BlueRaja-DannyPflughoeft:请参阅我的答案,但要直接回答:1)当您通过实现IDisposable 的其他对象间接使用这些资源时,或者当IDisposable 成语(和using 块的便利性)无论出于何种原因都可以使用。
  • @Adam:情况 1 正是需要清理非托管资源的情况。案例 2 与我的问题是正交的,无论如何都不是一个真实的例子。
  • @BlueRaja-DannyPflughoeft:是的,尽管模式会有所不同,具体取决于您是直接使用非托管资源还是通过抽象使用非托管资源。至于第二个,任何你有进入和退出点的模式都是合适的。虽然lock 已经存在,但很容易用using 重现相同类型的行为。它对于您暂时更改状态并希望确保将其更改回来的事情也很有用……例如,更改鼠标光标。您可以在构造函数中更改它并将其重置为 Dispose 中的任何内容。
  • @BlueRaja-DannyPflughoeft:对锁定对象的独占访问权资源;如果一个拥有独占访问权的对象在没有通知受保护对象不再需要独占性的情况下被放弃,那么没有人将能够再使用该对象。至于事件,如果无限数量的短期对象订阅了一个长期对象的事件,但很快就被放弃了,那么这些对象将造成无限的内存泄漏,因为它们在生命周期内没有资格被收集。寿命更长的物体。
【解决方案2】:

我认为从职责的角度考虑IDisposable 是最有帮助的。一个对象应该实现IDisposable,如果它知道在不再需要它和宇宙终结之间需要完成的事情(最好尽快),并且它是唯一具有这两个信息的对象并有动力去做。例如,打开文件的对象有责任查看文件是否关闭。如果对象只是在不关闭文件的情况下消失,则文件可能不会在任何合理的时间范围内关闭。

重要的是要注意,即使只与 100% 托管对象交互的对象也可以做需要清理的事情(并且应该使用 IDisposable)。例如,附加到集合的“已修改”事件的IEnumerator 在不再需要时需要将其自身分离。否则,除非枚举器使用一些复杂的技巧,否则只要收集在范围内,枚举器就永远不会被垃圾收集。如果集合被枚举一百万次,一百万个枚举器将附加到它的事件处理程序。

请注意,有时可以使用终结器进行清理,因为无论出于何种原因,在没有首先调用 Dispose 的情况下放弃了对象。有时这很好用;有时它工作得非常糟糕。例如,即使Microsoft.VisualBasic.Collection 使用终结器从“修改”事件中分离枚举器,尝试在没有干预Dispose 或垃圾收集的情况下枚举此类对象数千次将导致它变得非常慢——许多订单比正确使用Dispose 时的性能要慢很多。

【讨论】:

  • 谢谢,我希望我将 IEnumerator 示例视为反驳!
  • @SteveDunn:谢谢。似乎普遍认为“非托管资源”一词中的“非托管”一词与“非托管代码”有关。现实情况是,这两个概念在很大程度上是正交的。终结器可能会在某种程度上混淆“清理责任”问题,因为没有它们,“宇宙终结之前”的语言可能有点字面意思。如果一个没有终结器的对象拥有一个句柄的唯一副本,该句柄授予对某物的独占访问权,并且它在没有释放句柄的情况下被放弃,则句柄将真正永远被释放。
  • @SteveDunn:当然,手柄是否被释放的问题在宇宙终结之前可能会变得没有实际意义,但关键是一旦它的所有副本都消失了,什么都不会永远松开手柄。因此,具有它副本的最后一个实体必须确保在该句柄副本仍然存在时释放它。顺便说一句,完全在托管代码中的“非托管资源”的另一个很好的例子:锁。
  • even objects which only interact with 100% unmanaged objects 不应该读取 100% MANAGED 对象吗?否则,很好的答案。如果你的实现拥有一个 IDisposable 的实例,你也应该实现它来清理它,而 IDisposable 是唯一的沟通方式。
  • @Andy:已修复。我的主要观点是,很多人似乎认为“非托管资源”一词的意思是“由本机代码处理的资源”,而事实并非如此。虽然本机代码管理的资源几乎总是非托管资源,但它们并不是唯一重要的类型。实际上,我真的不喜欢“托管资源”和“非托管资源”这两个术语,因为它们的含义几乎没有一致性。不操纵自身外部任何东西的东西是“托管资源”,还是该术语仅指具有终结器的对象?
【解决方案3】:

所以,我的问题是,如果你的类型不包含 非托管资源,是否值得实现 IDisposable?

当有人在对象上放置 IDisposable 接口时,这告诉我创建者打算在该方法中做某事,或者将来他们可能打算这样做。为了确定,我总是在这种情况下调用 dispose 。即使它现在不做任何事情,但它可能在未来做任何事情,并且因为他们更新了一个对象而导致内存泄漏很糟糕,而且您在第一次编写代码时没有调用 Dispose。

事实上,这是一个判断电话。你不想过度实现它,因为在那个时候为什么还要麻烦有一个垃圾收集器。为什么不手动处理每个对象。如果您有可能需要处置非托管资源,那么这可能不是一个坏主意。这一切都取决于,如果唯一使用您的对象的人是您团队中的人,您可以随时跟进他们并说,“嘿,现在需要使用非托管资源。我们必须检查代码并确保我们已经收拾好了。”如果您将其发布以供其他组织使用,那就不同了。没有简单的方法可以告诉可能已经实现该对象的每个人,“嘿,您需要确保它现在已被处置。”让我告诉你,没有什么比升级第三方程序集更让人抓狂的了,因为他们更改了代码并让你的应用程序出现了内存问题。

我的同事回答说,在幕后,ADO.NET 连接是一个 托管资源。我对他的答复的答复是,一切最终 是非托管资源。

他是对的,它现在是一个托管资源。他们会改变它吗?谁知道呢,但打电话给它也无妨。我不会尝试猜测 ADO.NET 团队做了什么,所以如果他们把它放进去但它什么也没做,那很好。我还是会这样称呼它,因为一行代码不会影响我的工作效率。

您还遇到了另一种情况。假设您从一个方法返回一个 ADO.NET 连接。您不知道 ADO 连接是基础对象还是派生类型。您不知道 IDisposable 实现是否突然变得必要。无论如何,我总是这样称呼它,因为在生产服务器每 4 小时崩溃一次时,跟踪内存泄漏是很糟糕的。

【讨论】:

  • 许多类型实现 IDisposable 不是因为他们期望未来的版本可能会这样做,而是因为它们或基类型可以用作工厂方法的返回类型,这些工厂方法可能返回需要的派生类型清理。例如,所有实现IEnumerator<T> 的类型都实现了IDisposable,尽管它们的绝大多数Dispose 方法什么都不做。
【解决方案4】:

虽然已经有很好的答案,但我只是想明确一点。

实现IDisposable的三种情况:

  1. 您正在直接使用非托管资源。这通常涉及从 P/Invoke 调用中检索 IntPrt 或某种其他形式的句柄,该句柄必须由不同的 P/Invoke 调用释放
  2. 您正在使用其他 IDisposable 对象,需要对其处置负责
  3. 您还有其他需要或使用它,包括 using 块的便利性。

虽然我可能有点偏见,但你真的应该阅读(并展示给你的同事)the StackOverflow Wiki on IDisposable

【讨论】:

  • 我建议更新 Wiki 以将 生命周期管理 作为实施 IDisposable 的理由。例如,IObservable<T>.Subscribe 返回一个 IDisposable,即使它不打算封装非托管资源或用于 using 块。
  • @Gabe:这是一个 wiki,所以请随意编辑。我之前没用过IObservable<T>,所以如果你能添加一些东西可能会更好。
  • @AdamRobinson:应该澄清一下“总是”调用IDisposable。在对 IDisposable 的最后一个引用被销毁之前调用 Dispose 很重要(因为它不能在之后调用)。另一方面,对于许多对象来说,持有对IDisposable 的引用是很常见的;一般来说,确切的人应该打电话给Dispose
  • @supercat:是的,你是对的。任何给定的IDisposable 都应该有一个“所有者”,负责确保正确调用Dispose
【解决方案5】:

Dispose 应该用于生命周期有限的任何资源。终结器应该用于任何非托管资源。任何非托管资源都应该有一个有限的生命周期,但是有很多托管资源(如锁)也有有限的生命周期。

【讨论】:

    【解决方案6】:

    请注意,非托管资源很可能包括标准 CLR 对象,例如保存在一些静态字段中,所有这些对象都在安全模式下运行,根本没有非托管导入。

    没有简单的方法来判断实现IDiposable 的给定类是否真的需要清理某些东西。我的经验法则是总是在我不太了解的对象上调用 Dispose,比如一些 3rd 方库。

    【讨论】:

    • 值得注意的是,即使一个对象有DisposeRequired属性,找出Dispose是否必要所需的时间(略)会超过无条件调用Dispose所需的时间[因为它必须对属性进行虚拟调用,然后根据结果进行分支,而不是简单地进行虚拟调用]。 DisposeRequired 唯一有用的时候是确定 何时 调用 Dispose 会比实际调用它更麻烦(例如,如果需要清理的对象需要“用户计数”,但不适用于那些不这样做的人)。
    【解决方案7】:

    不,它 用于非托管资源。

    建议像框架调用的基本清理内置机制一样,使您可以清理您想要的任何资源,但最合适的自然是非托管资源管理。

    【讨论】:

    • 实现(和调用)Dispose 对于大多数资源持有类至关重要。托管/非托管大多无关紧要。
    • @HenkHolterman:对来说,我的意思似乎是完全:“它不仅适用于非托管资源管理。”不是吗?
    • 是的,对不起。我在那里忽略了一个不是
    【解决方案8】:

    如果您聚合IDisposables,那么您应该实现接口以便及时清理这些成员。在您引用的 ADO.Net 连接示例中,myConn.Dispose() 将如何被调用?

    我认为说一切都是非托管资源在这种情况下是不正确的。我也不同意你同事的观点。

    【讨论】:

      【解决方案9】:

      你是对的。托管数据库连接、文件、注册表项、套接字等都保留在非托管对象上。这就是他们实施IDisposable 的原因。如果你的类型拥有一次性对象,你应该实现 IDisposable 并在你的 Dispose 方法中处理它们。否则它们可能会一直存活,直到垃圾收集导致锁定文件和其他意外行为。

      【讨论】:

      • 呃,听起来你同意OP的同事,而不是OP。
      • 好吧,我假设同事争辩说,如果您的类型具有托管 ADO 连接,则不必实现 IDisposable,因为资源是托管的。我说的是你必须这样做,因为在任何IDisposable 对象的深处都有一个非托管资源。当您聚合 IDisposable 对象时,您还必须实现 IDisposable
      • 在这种情况下,有非托管资源需要清理。同事争辩说,如果没有非托管资源,则不需要IDisposable - OP 似乎认为 "即使没有非托管资源,实现 IDisposable [..] 也是有价值的。清理干净。”
      • @BlueRaja-DannyPflughoeft:我想这个问题有点模棱两可。我的问题主要是对我对他的答复的答复是,一切最终都是非托管资源。我支持这种说法。
      【解决方案10】:

      一切最终都是非托管资源。

      不正确。除了 CLR 对象使用的内存之外的所有内容,仅由框架管理(分配和释放)。

      实现IDisposable 和调用Dispose在不持有任何非托管资源的对象上(直接或间接通过依赖对象)毫无意义。它确实使释放该对象确定性,因为您不能自己直接释放对象的 CLR 内存,因为它总是只有 GC这样做。对象是引用类型,因为值类型在直接在方法级别使用时,由堆栈操作分配/释放。

      现在,每个人都声称自己的答案是正确的。让我证明我的。根据documentation

      Object.Finalize 方法允许对象在被垃圾回收器回收之前尝试释放资源并执行其他清理操作

      换句话说,对象的 CLR 内存在 Object.Finalize() 被调用后被释放。 [注意:如果需要,可以显式跳过此调用]

      这是一个没有非托管资源的一次性类:

      internal class Class1 : IDisposable
      {
          public Class1()
          {
              Console.WriteLine("Construct");
          }
      
          public void Dispose()
          {
              Console.WriteLine("Dispose");
          }
      
          ~Class1()
          {
              Console.WriteLine("Destruct");
          }
      }
      

      注意destructor 隐式调用继承链中的每个FinalizeObject.Finalize()

      这是控制台应用程序的Main 方法:

      static void Main(string[] args)
      {
          for (int i = 0; i < 10; i++)
          {
              Class1 obj = new Class1();
              obj.Dispose();
          }
      
          Console.ReadKey();
      }
      

      如果调用Dispose 是一种以确定性方式释放托管 对象的方法,那么每次“Dispose”都会紧跟“Destruct”,对吗?亲眼看看会发生什么。从命令行窗口运行这个应用程序是最有趣的。

      注意:有一种方法可以强制GC 收集当前应用程序域中等待完成的所有对象,但不收集单个特定对象。不过,您无需调用Dispose 即可在完成队列中拥有一个对象。强烈建议不要强制收集,因为这可能会损害应用程序的整体性能。

      编辑

      有一个例外 - 状态管理。如果您的对象碰巧管理外部状态,Dispose 可以处理状态更改。即使 state 不是一个非托管对象,由于 IDisposable 的特殊处理,使用它也非常方便。例如安全上下文或模拟上下文。

      using (WindowsImpersonationContext context = SomeUserIdentity.Impersonate()))
      {
          // do something as SomeUser
      }
      
      // back to your user
      

      这不是最好的例子,因为WindowsImpersonationContext 在内部使用系统句柄,但你明白了。

      底线是在实现IDisposable 时,您需要(或计划)在Dispose 方法中做一些有意义的事情。否则只是浪费时间。 IDisposable 不会改变 GC 管理对象的方式。

      【讨论】:

      • 你不在这里。该对象可能不是“物理上”垃圾收集的,但从逻辑上讲,它会在已知时间释放一些重要的东西。这个“东西”可能不仅是一些非托管资源,还可能是队列中的一个槽、为某些计算分配线程池线程、使用其他程序的许可等——即,一个逻辑资源。内存中对象的存在是一个纯粹的细节,如果一次性对象以正确的方式实现,则不应考虑。
      • @Vlad 你错过了我回答的某些部分。您提到的所有这些“东西”最终都是非托管资源。如果您的对象处理那些(直接或间接通过依赖对象),它将成为非托管对象并应实现Dispose。换句话说,如果任何依赖对象实现 Dispose 您的对象也应该实现。抱歉,如果没有明确指出。
      • 好吧,我不是在质疑你的全部答案,它只是“在不保留任何非托管资源 (...) 的对象上调用 Dispose 毫无意义”的一部分。我试图获得一些资源基本上是受管理的示例,尽管需要以确定的方式释放。例如,修改集合的权限显然不是非托管资源,但在RAII-wayIDisposable 中获得此权限是一件好事:using (ObtainModifyRight(collection)) { /* modify it */ }
      • 使用 Dispose 管理状态有点劫持 dispose 模式,因为它适用于非托管资源。但我同意这种情况有时会发生并且很方便,因为 using 关键字。正在更新答案。
      • 修改集合的权限是非托管资源,如果授予一个对象的权限会削弱其他对象修改该集合的能力,直到收到该权限的对象表示不再需要它.毕竟,一个对象向操作系统请求一块非托管内存的真正含义是什么,除了(1)操作系统授予对象使用该内存的权利,以及(2)其他人将无法使用该内存。使用它直到第一个对象告诉操作系统它不再需要它?
      【解决方案11】:

      如果你的 Type 引用了非托管资源,或者它持有对实现 IDisposable 的对象的引用,则它应该实现 IDisposable。

      【讨论】:

        【解决方案12】:

        在我的一个项目中,我有一个包含托管线程的类,我们将它们称为线程 A 和线程 B,以及一个 IDisposable 对象,我们将其称为 C。

        A 用于在退出时处理 C。 B 曾经使用 C 来保存异常。

        我的班级必须实现 IDisposable 和一个描述符,以确保以正确的顺序处理事物。 是的,GC 可以清理我的物品,但我的经验是除非我管理我的班级清理工作,否则会出现竞争情况。

        【讨论】:

          【解决方案13】:

          简短回答:绝对不是。如果您的类型具有托管或非托管成员,则应实现 IDisposable。

          现在详情: 我已经回答了这个问题,并在 StackOverflow 上提供了有关内存管理内部和 GC 问题的更多详细信息。这里只是一些:

          关于 IDisposable 实现的最佳实践,请参考我的博文:

          How do you properly implement the IDisposable pattern?

          【讨论】:

            【解决方案14】:

            根本不需要资源(托管或非托管)。通常,IDisposable 只是一种方便的方式,可以消除 try {..} finally {..} 的繁琐,只需比较一下:

              Cursor savedCursor = Cursor.Current;
            
              try {
                Cursor.Current = Cursors.WaitCursor;
            
                SomeLongOperation();
              }
              finally {
                Cursor.Current = savedCursor;
              }
            

              using (new WaitCursor()) {
                SomeLongOperation();
              }
            

            其中WaitCursorIDisposable 适合using

              public sealed class WaitCursor: IDisposable {
                private Cursor m_Saved;
            
                public Boolean Disposed {
                  get;
                  private set;
                }
            
                public WaitCursor() {
                  Cursor m_Saved = Cursor.Current;
                  Cursor.Current = Cursors.WaitCursor;
                }
            
                public void Dispose() {
                  if (!Disposed) {
                    Disposed = true;
                    Cursor.Current = m_Saved;
                  }
                }
              }
            

            您可以轻松地组合这样的类:

              using (new WaitCursor()) {
                using (new RegisterServerLongOperation("My Long DB Operation")) {
                  SomeLongRdbmsOperation();  
                }
            
                SomeLongOperation();
              }
            

            【讨论】:

              【解决方案15】:

              如果对象拥有任何非托管对象任何托管一次性对象

              ,则实现IDisposable

              如果一个对象使用非托管资源,它应该实现IDisposable。拥有一次性对象的对象应实现IDisposable 以确保释放底层非托管资源。如果遵循规则/约定,因此可以得出结论,不释放托管一次性对象等于不释放非托管资源是合乎逻辑的。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2013-05-12
                • 2013-02-02
                • 2015-10-20
                • 2020-02-15
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-03-26
                相关资源
                最近更新 更多