在C#中使用IDisposable和析构函数有什么区别?


101

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

我的假设是,如果我在对象上实现IDispose,则可以显式地“销毁”该对象,而不是等待垃圾回收器执行此操作。这样对吗?

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


5
实际上,您应该在每个Disposable对象上调用Dispose。您可以使用该using构造轻松地做到这一点。
Luc Touraille

嗯,那很有道理。我一直想知道为什么将'using'语句用于文件流。我知道它与对象的范围有关,但是我没有将它放在IDisposable接口的上下文中。
乔丹·帕默

5
要记住的重要一点是,终结器永远不要访问类的任何托管成员,因为这些成员可能不再是有效的引用。
丹·布莱恩特2010年

Answers:


126

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

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

之前有很多主题:

最后,请注意,IDisposable对象也具有终结器并不罕见。在这种情况下,Dispose()通常调用GC.SuppressFinalize(this),这意味着GC不会运行终结器-它只会丢弃内存(便宜得多)。如果您忘记了Dispose()该对象,则终结器仍将运行。


谢谢!这是很合理的。我非常感谢您的大力回应。
乔丹·帕默

27
还有一件事要说。除非确实需要一个终结器,否则不要在其类中添加终结器。如果添加终结器(析构函数),则GC必须调用它(甚至是空的终结器),然后调用它,该对象将始终在第1代垃圾回收中幸存。这将阻止GC并降低其速度。这是Marc说要在上述代码中致电SuppressFinalize的
Kevin Jones

1
因此,完成是释放非托管资源。但是Dispose可用于释放托管和非托管资源吗?
Dark_Knight

2
@黑暗是的; 因为6级管理链可能是未经管理的,需要及时清理
Marc Gravell

1
@KevinJones带有终结器的对象可以保证在第0代而不是第1代生存,对吗?我在一本书中读到了.NET Performance。
David Klempfner '18

25

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


9

MSDN上有一个很好的描述:

此接口的主要用途是释放非托管资源。当不再使用托管对象时,垃圾收集器会自动释放分配给该对象的内存。但是,无法预测何时会发生垃圾回收。此外,垃圾收集器不了解 诸如窗口句柄或打开的文件和流之类的非托管资源

使用此接口的Dispose方法 与垃圾回收器一起显式释放非托管资源。当不再需要该对象时,该对象的 使用者可以调用此方法。


1
该描述的一个主要弱点是MS给出了非托管资源的示例,但是据我所知,它从未真正定义该术语。由于托管对象通常只能在托管代码中使用,因此人们可能会认为非托管代码中使用的东西是非托管资源,但这并不是真的。许多非托管代码不使用任何资源,某些非托管资源(例如事件)仅存在于托管代码领域中。
超级猫

1
如果短期对象订阅了长期对象的事件(例如,它要求在短期对象的生存期内发生任何更改,则通知该事件),则此类事件应被视为非托管资源,因为取消订阅该事件将导致短期对象的生存期延长到长期对象的生存期。如果成千上万的短期对象订阅了一个事件,但是在没有取消订阅的情况下被放弃,则可能导致内存或CPU泄漏(因为处理每个订阅所需的时间会增加)。
2015年

1
在托管代码中涉及非托管资源的另一种情况是从池中分配对象。特别是如果代码需要在.NET Micro Framework中运行(其垃圾收集器的效率远低于台式机上的垃圾收集器),则代码具有例如结构数组可能会有所帮助,每个结构都可以标记为“已使用”或“免费”。分配请求应该找到一个当前标记为“空闲”的结构,将其标记为“已使用”,并返回一个索引。发布请求应将结构标记为“免费”。如果分配请求返回,例如23,则……
supercat

1
...如果代码从不通知数组的所有者它不再需要项目#23,则该数组插槽将永远无法被其他任何代码使用。由于GC非常有效,因此在桌面代码中并不经常使用这种从数组插槽中进行手动分配的方法,但是在Micro Framework上运行的代码中,这种方法可以产生很大的不同。
2015年


4

关于是否应始终致电Dispose的问题通常是激烈的辩论。请参见博客为从.NET社区尊重个人一个有趣的视角。

就我个人而言,我认为杰弗里·里希特(Jeffrey Richter)的观点Dispose是非强制性的,这一点极其薄弱。他举两个例子来证明自己的观点。

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

在第二个示例中,他指出,开发人员可能错误地假定IAsyncResult.WaitHandle应该主动处置该实例,而没有意识到该属性懒惰地初始化了等待句柄,从而导致不必要的性能损失。但是,此示例的问题在于,它IAsyncResult本身不遵守Microsoft自己发布的有关处理IDisposable对象的准则。也就是说,如果类持有对IDisposable类型的引用,则该类本身应实现IDisposable。如果IAsyncResult遵循该规则,那么它自己的Dispose方法就可以决定需要处置哪个组成成员。

因此,除非有人有更令人信服的论据,否则我将停留在“总是称为Dispose”阵营中,因为要理解会有一些附带情况的情况主要是由不良的设计选择引起的。


3

真的很简单。我知道已经回答了,但是我会再试一次,但是会尽量简化。

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

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

在执行终结器时,大多数类都将调用dispose,但这只是作为安全保护措施而已,绝不应该依赖它。完成IDisposable之后,您应该明确处置任何实现IDisposable的东西。如果确实实现IDisposable,则应在终结器中调用dispose。有关示例,请参见http://msdn.microsoft.com/zh-cn/library/system.idisposable.aspx


不,垃圾回收器从不调用Dispose()。它调用终结器。
马克·Gravell

固定的。类应该在其终结器中调用dispose,但不必这样做。
DaEagle

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.