这里应该做一些更正:
关于 Phil Devaney 的回答:
“...调用 Dispose 可让您进行确定性清理,强烈推荐。”
实际上,调用 Dispose() 不会确定性地导致 .NET 中的 GC 收集 - 即它不会仅仅因为您调用 Dispose() 而立即触发 GC。它只是间接地向 GC 发出信号,表明可以在下一次 GC 期间清理对象(对于对象所在的 Generation)。换句话说,如果对象存在于 Gen 1 中,那么在 Gen 1 收集发生之前它不会被处置。您可以以编程方式和确定性地使 GC 执行收集的唯一方法之一(尽管不是唯一的)是调用 GC.Collect()。但是,不建议这样做,因为 GC 在运行时通过收集有关应用程序运行时内存分配的指标来“调整”自身。调用 GC.Collect() 会转储这些指标并导致 GC 重新开始其“调整”。
关于答案:
IDisposable 用于处置非托管资源。这是 .NET 中的模式。
这是不完整的。由于 GC 是不确定的,因此可以使用 Dispose 模式 (How to properly implement the Dispose pattern),以便您可以释放正在使用的资源 - 托管或非托管。它与您发布的资源类型无关。实现终结器的需求确实与您使用的资源类型有关 - 即,只有在您拥有不可终结(即本机)资源时才实施一个。也许你混淆了两者。顺便说一句,您应该避免使用 SafeHandle 类来实现终结器,而是使用封装通过 P/Invoke 或 COM 互操作封送的本机资源。如果您最终实现了终结器,则应该始终实现 Dispose 模式。
我还没有看到任何人提到的一个重要注意事项是,如果一次性对象被创建并且它有一个终结器(你永远不知道他们是否这样做 - 你当然不应该对那),然后它将被直接发送到终结队列并至少存在 1 个额外的 GC 集合。
如果最终没有调用 GC.SuppressFinalize(),则对象的终结器将在下一次 GC 时调用。请注意,Dispose 模式的正确实现应该调用 GC.SuppressFinalize()。因此,如果您在对象上调用 Dispose(),并且它已经正确实现了模式,您将避免执行终结器。如果您不对具有终结器的对象调用 Dispose(),则该对象的终结器将由 GC 在下一次收集时执行。为什么这很糟糕? CLR 中的终结器线程(包括 .NET 4.6)是单线程的。想象一下,如果您增加此线程的负担会发生什么 - 您的应用性能取决于您知道的位置。
对对象调用 Dispose 可提供以下功能:
- 减少过程中 GC 的应变;
- 降低应用的内存压力;
- 如果 LOH(大对象堆)碎片化并且对象位于 LOH 上,则减少 OutOfMemoryException (OOM) 的机会;
- 如果对象具有终结器,则将其排除在可终结和可访问队列之外;
- 确保清理您的资源(托管和非托管)。
编辑:
我刚刚注意到IDisposable 上的“无所不知且始终正确”的 MSDN 文档(此处极度讽刺)实际上确实说
这个接口的主要用途是
释放非托管资源
正如任何人都应该知道的那样,MSDN 远非正确,从未提及或展示“最佳实践”,有时会提供无法编译的示例等。不幸的是,这些话中记录了这一点。但是,我知道他们想说什么:在一个完美的世界里,GC 会为你清理所有托管资源(多么理想化);但是,它不会清理 非托管 资源。这是绝对正确的。话虽如此,生活并不完美,任何应用也不完美。 GC 只会清理没有根引用的资源。这主要是问题所在。
在 .NET 可以“泄漏”(或不释放)内存的大约 15-20 种不同方式中,如果您不调用 Dispose(),最有可能会咬到您的一种方式是无法取消注册/取消挂钩/取消连接/detach 事件处理程序/委托。如果您创建了一个对象,该对象具有连接到它的委托,并且您没有在其上调用 Dispose()(并且不要自己分离委托),则 GC 仍会将对象视为具有根引用 - 即委托。因此,GC 永远不会收集它。
@joren 的评论/问题如下(我的回复太长,无法评论):
我有一篇关于我推荐使用的 Dispose 模式的博文 - (How to properly implement the Dispose pattern)。有时您应该取消引用,这样做永远不会有坏处。实际上,这样做确实在 GC 运行之前做了一些事情——它删除了对该对象的根引用。 GC 稍后扫描它的根引用集合并收集那些没有根引用的引用。最好考虑一下这个例子:你有一个“ClassA”类型的实例——我们称它为“X”。 X 包含一个“ClassB”类型的对象——我们称之为“Y”。 Y 实现了 IDisposable,因此,X 应该做同样的事情来处理 Y。让我们假设 X 在第 2 代或 LOH 中并且 Y 在第 0 代或第 1 代中。当在 X 上调用 Dispose() 并且该实现使对 Y 的引用,对 Y 的根引用会立即被删除。如果第 0 代或第 1 代发生 GC,则 Y 的内存/资源会被清理,但 X 的内存/资源不会因为 X 位于第 2 代或 LOH 中。