在C#中,使用IDisposable与使用析构函数的区别是什么?

115

什么情况下我应该在类上实现IDispose而不是析构函数?我阅读了这篇文章,但我仍然没有理解重点。

我的假设是,如果我在对象上实现了IDispose,那么我可以显式地“销毁”它,而不是等待垃圾回收器来处理。这个假设正确吗?

这是否意味着我应该总是显式调用对象的Dispose方法?有哪些常见的例子?


6
确实,您应该在每个可处置对象上调用Dispose。您可以使用“using”结构很容易地完成这项工作。 - Luc Touraille
啊,这很有道理。我一直想知道为什么在文件流中使用'using'语句。我知道它与对象的作用域有关,但我没有将其与IDisposable接口联系起来。 - Jordan Parmer
6
一个重要的点需要记住,即终结器不应该访问类的任何托管成员,因为这些成员可能不再是有效的引用。 - Dan Bryant
8个回答

148
一个 finalizer(又称为析构函数)是垃圾回收的一部分,它的执行时间不确定,因为垃圾回收主要是由于内存压力(即需要更多的空间)而发生的。Finalizer 通常仅用于清理非托管资源,因为托管资源将具有自己的收集/处理。
因此,IDisposable 用于确定性地清理对象,即立即进行。它不会收集对象的内存(这仍然属于 GC),但例如用于关闭文件、数据库连接等。
关于此主题已经有很多之前的话题: 最后,请注意,一个 IDisposable 对象通常也会有一个finalizer;在这种情况下,Dispose() 通常调用 GC.SuppressFinalize(this),这意味着 GC 不运行 finalizer - 它只是丢弃内存(成本更低)。如果您忘记了 Dispose() 对象,则 finalizer 仍然会运行。

谢谢!那很有道理。非常感谢您的出色回复。 - Jordan Parmer
32
还有一点需要说明,除非你确实非常需要,否则不要给你的类添加终结器。如果你添加了一个终结器(析构函数),GC 就必须调用它(即使是空的终结器),而为了调用它,对象总是会在第一代垃圾回收时幸存下来。这将妨碍和减慢 GC 的执行速度。这就是马克建议在上面的代码中调用 SuppressFinalize 的原因。 - Kevin Jones
1
所以Finalize是释放非托管资源的。但是Dispose可以用来释放托管和非托管资源吗? - Dark_Knight
2
@Dark 是的;因为管理链下面的6个级别可能是一个需要及时清理的未受管理的级别。 - Marc Gravell
1
@KevinJones 对象带有终结器会保证在第0代中存活,而不是第1代,对吗?我在一本名为.NET性能的书中读到了这个。 - David Klempfner

28

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


需要注意的是,必须编写一个终结器来调用.Dispose()。不能明确假设它会被调用。 - Enigmativity

9

MSDN上有一个非常好的描述:

这个接口的主要用途是释放非托管资源。当不再使用托管对象时,垃圾收集器会自动释放分配给该对象的内存。然而,无法预测垃圾回收何时发生。此外,垃圾收集器没有关于非托管资源(如窗口句柄或打开的文件和流)的知识。

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


1
该描述的一个主要弱点是MS提供了未管理资源的示例,但从我所看到的情况来看,它从未真正定义这个术语。由于托管对象通常只能在托管代码中使用,因此人们可能认为在非托管代码中使用的东西是未管理资源,但这并不完全正确。许多非托管代码不使用任何资源,某些类型的非托管资源(例如事件)仅存在于托管代码世界中。 - supercat
1
如果一个短暂存在的对象订阅了一个长期存在的对象的事件(例如,它要求在短暂存在对象的生命周期内通知任何更改),这样的事件应被视为未受管理的资源,因为未取消订阅事件将导致短暂存在对象的生命周期延长到长期存在对象的生命周期。如果成千上万或数百万个短暂存在的对象订阅了一个事件,但在不取消订阅的情况下被放弃,那么可能会导致内存或 CPU 泄漏(因为处理每个订阅所需的时间会增加)。 - supercat
1
涉及管理代码中未管理资源的另一种情况是从池分配对象。尤其是如果代码需要在.NET Micro Framework中运行(其垃圾收集器比桌面机器上的收集器要低效得多),那么对于代码来说,拥有例如结构体数组可能会很有帮助,每个结构体都可以被标记为“已使用”或“可用”。分配请求应该找到一个当前标记为“可用”的结构体,将其标记为“已使用”,并返回其索引;释放请求应该将结构标记为“可用”。如果分配请求返回例如23,那么... - supercat
1
如果代码从未通知数组所有者它不再需要第23项,那么该数组槽位将永远无法被任何其他代码使用。这种手动分配数组槽位在桌面代码中并不经常使用,因为垃圾收集器非常高效,但在运行于微型框架上的代码中,它可以产生巨大的影响。 - supercat

9
在C#析构函数中应该只包含这一行代码:
Dispose(False);

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


3
这是微软在.NET文档中提出的设计模式,但当您的对象不是IDisposable时,请不要使用它。http://msdn.microsoft.com/en-us/library/fs2xkftw%28v=vs.110%29.aspx - Zbyl
1
我想不出为什么会提供一个具有终结器但没有Dispose方法的类。 - Jonathan Allen

5
关于是否应该总是调用Dispose的问题,通常会引起激烈的争论。请参考.NET社区中备受尊敬的人士在博客中提供的有趣观点。
个人认为,Jeffrey Richter认为调用Dispose不是必须的立场非常薄弱。他举了两个例子来证明自己的观点。
在第一个例子中,他说在Windows Forms控件上调用Dispose在主流情况下是繁琐且不必要的。然而,他没有提到在这些主流情况下,控件容器实际上会自动调用Dispose
在第二个例子中,他指出开发人员可能会错误地假设从IAsyncResult.WaitHandle返回的实例需要被积极地释放,而没有意识到该属性惰性初始化等待句柄,导致不必要的性能损失。但是,这个例子的问题在于IAsyncResult本身并没有遵循Microsoft对处理IDisposable对象的公布指南。也就是说,如果一个类持有对IDisposable类型的引用,那么该类本身应该实现IDisposable。如果IAsyncResult遵循这个规则,它自己的Dispose方法就可以决定哪些成员需要释放。
因此,除非有更有力的论据,否则我将坚持“总是调用Dispose”的立场,并理解在大多数情况下都会出现一些边缘情况,这主要是由于糟糕的设计选择所导致的。

3

这其实很简单。我知道已经有答案了,但我会尽可能地保持简单。

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

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

大多数类将在执行终结器时调用 dispose,但这仅作为一种安全措施存在,不应该依赖于它。当您完成使用实现 IDisposable 的任何内容时,应显式地调用 dispose。如果您实现了 IDisposable,则应在终结器中调用 dispose。请参见 http://msdn.microsoft.com/en-us/library/system.idisposable.aspx 以获取示例。


不,垃圾回收器从不调用Dispose()方法。它会调用终结器。 - Marc Gravell
已修复。类应该在其终结器中调用dispose,但不一定非得这样做。 - DaEagle

0

0
为什么有些人使用Finalize方法而不是Dispose方法?
为了确保垃圾回收器(GC)能够快速收集对象。
如果你的类中有Finalize方法,GC将会进行两轮来收集该对象。首先,它会检查对象是否未被使用,当它发现Finalize方法时,不会立即清除该对象,而是调用Finalize方法,并告诉对象在下一轮将被清除。然后,在下一轮中,GC会清除该对象。
在什么情况下会使用Finalize方法而不是Dispose方法,反之亦然?
当你有Finalize方法时,你应该始终使用Dispose方法。它们可以相互补充地工作。Finalize和Dispose的组合被称为Finalize/Dispose模式。
一些需要记住的要点:
1. 当你没有非托管对象时,不要使用Finalize。 2. 如果你需要析构函数(Finalize),请确保使用IDisposable公开Dispose方法。 3. 不要忘记在Dispose方法中调用GC.SuppressFinalize。 4. 在客户端代码中,使用USING语句包装对象调用,以便自动调用Dispose方法。

当你亲眼目睹而非在脑海中想象时,像垃圾回收(GC)这样的概念会更容易理解。我制作了一个关于垃圾回收器的视频以更实践的方式回答这些问题。你可以从第10个问题开始观看,那里我开始谈论Dispose模式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接