【问题标题】:What is the difference between using IDisposable vs a destructor in C#?在 C# 中使用 IDisposable 与析构函数有什么区别?
【发布时间】:2010-09-25 05:47:09
【问题描述】:

什么时候在类上实现 IDispose 而不是析构函数?我读了this article,但我仍然没有抓住重点。

我的假设是,如果我在一个对象上实现 IDispose,我可以显式地“破坏”它,而不是等待垃圾收集器执行它。这是正确的吗?

这是否意味着我应该始终在对象上显式调用 Dispose?有哪些常见的例子?

【问题讨论】:

  • 确实,您应该在每个 Disposable 对象上调用 Dispose。您可以使用 using 构造轻松做到这一点。
  • 啊,有道理。我一直想知道为什么“使用”语句用于文件流。我知道它与对象的范围有关,但我没有将它放在 IDisposable 接口的上下文中。
  • 要记住的重要一点是终结器应该永远访问类的任何托管成员,因为这些成员可能不再是有效的引用。

标签: c# .net dispose destructor


【解决方案1】:

终结器(又名析构函数)是垃圾回收 (GC) 的一部分 - 它何时(或什至)发生这种情况是不确定的,因为 GC 主要是由于内存压力(即需要更多空间)而发生的。终结器通常仅用于清理非托管资源,因为托管资源将有自己的收集/处置。

因此IDisposable 用于确定地清理对象,即现在。它不收集对象的内存(仍然属于 GC) - 但用于关闭文件、数据库连接等。

这方面有很多以前的话题:

最后,请注意IDisposable 对象也有终结器的情况并不少见;在这种情况下,Dispose() 通常调用GC.SuppressFinalize(this),这意味着 GC 不会运行终结器——它只是将内存扔掉(便宜得多)。如果您忘记Dispose() 对象,终结器仍会运行。

【讨论】:

  • 谢谢!这很有意义。我非常感谢您的好评。
  • 还有一点要说。除非您真的非常需要,否则不要在您的课程中添加终结器。如果你添加一个终结器(析构函数),GC 必须调用它(甚至是一个空的终结器)并且调用它时,对象将始终在第 1 代垃圾收集中幸存下来。这将阻碍和减慢 GC。这就是 Marc 所说的在上面的代码中调用 SuppressFinalize
  • 所以Finalize就是释放非托管资源。但是 Dispose 可以用来释放托管和非托管资源吗?
  • @Dark 是的;因为管理链下的 6 个级别可能是需要及时清理的非托管级别
  • @KevinJones 带有终结器的对象可以保证在第 0 代而不是 1 代中存活,对吗?我在一本名为 .NET Performance 的书中读到了这一点。
【解决方案2】:

Finalize() 方法的作用是确保 .NET 对象可以在垃圾收集时清理非托管资源。但是,应该尽快释放数据库连接或文件处理程序等对象,而不是依赖垃圾收集。为此,您应该实现IDisposable 接口,并在Dispose() 方法中释放您的资源。

【讨论】:

    【解决方案3】:

    C# 析构函数中唯一应该包含的就是这一行:

    Dispose(False);
    

    就是这样。该方法中不应该有其他任何东西。

    【讨论】:

    【解决方案4】:

    MSDN上有很好的描述:

    这个接口的主要用途是 释放非托管资源。 垃圾收集器自动 释放分配给 a 的内存 当该对象为 no 时的托管对象 使用时间更长。然而,不是 可以预测什么时候垃圾 收集将发生。此外, 垃圾收集器没有 非托管资源的知识 例如窗口句柄,或 open 文件和流。

    使用这个的 Dispose 方法 显式释放的界面 非托管资源 与垃圾收集器。这 对象的消费者可以在对象不存在时调用该方法 需要更长的时间。

    【讨论】:

    • 该描述的一个主要弱点是 MS 提供了非托管资源的示例,但据我所知,从未真正定义过该术语。由于托管对象通常只能在托管代码中使用,人们可能会认为非托管代码中使用的东西是非托管资源,但事实并非如此。许多非托管代码不使用任何资源,并且某些类型的非托管资源(例如事件)仅存在于托管代码世界中。
    • 如果一个短生命周期的对象从一个长生命周期的对象订阅一个事件(例如,它要求通知在短生命周期对象的生命周期内发生的任何变化),这样的事件应该是被认为是非托管资源,因为未能取消订阅事件会导致短期对象的生命周期延长到长期对象的生命周期。如果成千上万或数百万个短期对象订阅了一个事件,但没有取消订阅就被放弃了,这可能会导致内存或 CPU 泄漏(因为处理每个订阅所需的时间会增加)。
    • 托管代码中涉及非托管资源的另一种情况是从池中分配对象。尤其是如果代码需要在 .NET Micro Framework 中运行(其垃圾收集器的效率远低于台式机上的垃圾收集器),则代码可能会有所帮助,例如一个结构数组,每个结构都可以标记为“已使用”或“空闲”。分配请求应该找到一个当前标记为“空闲”的结构,将其标记为“已使用”,并返回一个索引给它;发布请求应将结构标记为“免费”。如果分配请求返回,例如23,然后……
    • ...如果代码从未通知数组的所有者它不再需要第 23 项,则该数组插槽将永远无法被任何其他代码使用。由于 GC 非常高效,因此在桌面代码中并不经常使用这种手动分配数组插槽,但在 Micro Framework 上运行的代码中,它可以产生巨大的差异。
    【解决方案5】:

    您是否应该始终致电Dispose 的问题通常是一场激烈的辩论。请参阅 this 博客,了解 .NET 社区中受人尊敬的个人的有趣观点。

    就我个人而言,我认为 Jeffrey Richter 的立场是,调用 Dispose 不是强制性的,这非常弱。他举了两个例子来证明他的观点。

    在第一个示例中,他说在 Windows 窗体控件上调用 Dispose 在主流场景中是乏味且不必要的。但是他没有提到Dispose实际上是在那些主流场景中被控制容器自动调用的。

    在第二个示例中,他指出开发人员可能错误地认为应该积极处置来自IAsyncResult.WaitHandle 的实例,而没有意识到该属性会延迟初始化等待句柄,从而导致不必要的性能损失。但是,这个例子的问题是IAsyncResult 本身不遵守微软自己发布的处理IDisposable 对象的准则。也就是说,如果一个类拥有对IDisposable 类型的引用,那么该类本身应该实现IDisposable。如果IAsyncResult 遵循该规则,那么它自己的Dispose 方法就可以决定需要处置哪些组成成员。

    因此,除非有人有更令人信服的论点,否则我将留在“总是调用 Dispose”阵营,并理解将出现一些边缘案例,主要是由于糟糕的设计选择。

    【讨论】:

      【解决方案6】:

      真的很简单。我知道它已得到答复,但我会再试一次,但会尽量保持简单。

      通常不应该使用析构函数。它只是运行.net 想要它运行。它只会在垃圾回收周期后运行。它可能永远不会在您的应用程序的生命周期中真正运行。出于这个原因,您不应该将任何代码放入“必须”运行的析构函数中。您也不能依赖类中的任何现有对象在运行时存在(它们可能已经被清理,因为不保证析构函数的运行顺序)。

      只要您有一个创建需要清理的资源(即文件和图形句柄)的对象,就应该使用IDisposible。事实上,许多人认为,由于上述原因,您放入析构函数中的任何内容都应该放入 IDisposable。

      大多数类会在终结器执行时调用 dispose ,但这只是作为安全防护,永远不应依赖。完成后,您应该明确处置任何实现 IDisposable 的东西。如果你确实实现了 IDisposable,你应该在终结器中调用 dispose。示例见http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

      【讨论】:

      • 不,垃圾收集器从不调用 Dispose()。它调用终结器。
      • 修复了这个问题。类应该在它们的终结器中调用 dispose,但它们不是必须的。
      【解决方案7】:

      这是另一篇很好的文章,它清除了 IDisposable、GC 和 dispose 周围的一些迷雾。

      Chris Lyons WebLog Demystifying Dispose

      【讨论】:

        猜你喜欢
        • 2018-10-30
        • 1970-01-01
        • 1970-01-01
        • 2013-05-30
        • 2023-03-10
        • 2021-06-01
        • 2023-03-18
        • 2010-11-07
        相关资源
        最近更新 更多