由于.NET有垃圾收集器,为什么我们需要终结器/析构函数/ dispose-pattern?

时间:2008-12-01 18:47:16

标签: c# .net memory memory-management garbage-collection

如果我理解正确,.net运行时将始终在我之后清理。因此,如果我创建新对象并停止在我的代码中引用它们,运行时将清理这些对象并释放它们占用的内存。

既然这就是为什么一些对象需要有一个析构函数或dispose方法呢?当它们不再被引用时,运行时不会清理它们吗?

12 个答案:

答案 0 :(得分:93)

需要终结器来保证将稀缺资源释放回系统,如文件句柄,套接字,内核对象等。由于终结器总是在对象生命周期结束时运行,因此它是释放这些句柄的指定位置。

Dispose模式用于提供确定性的资源破坏。由于.net运行时垃圾收集器是非确定性的(这意味着您永远无法确定运行时何时将收集旧对象并调用其终结器),因此需要一种方法来确保系统资源的确定性释放。因此,当您正确实现Dispose模式时,您可以提供资源的确定性释放,并且在消费者粗心并且不处理对象的情况下,终结器将清理对象。

为什么需要Dispose的一个简单示例可能是一个快速而又脏的日志方法:

public void Log(string line)
{
    var sw = new StreamWriter(File.Open(
        "LogFile.log", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None));

    sw.WriteLine(line);

    // Since we don't close the stream the FileStream finalizer will do that for 
    // us but we don't know when that will be and until then the file is locked.
}

在上面的示例中,文件将保持锁定状态,直到垃圾收集器调用StreamWriter对象上的终结器。这提出了一个问题,因为在此期间,可能会再次调用该方法来编写日志,但这次它会因为文件仍然被锁定而失败。

正确的方法是在完成后使用它来处置对象:

public void Log(string line)
{
    using (var sw = new StreamWriter(File.Open(
        "LogFile.log", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))) {

        sw.WriteLine(line);
    }

    // Since we use the using block (which conveniently calls Dispose() for us)
    // the file well be closed at this point.
}
顺便说一下,技术上的终结者和破坏者意味着同样的事情;我更喜欢称c#destructors'终结器',否则它们往往会让人混淆C ++析构函数,这与C#不同,是确定性的。

答案 1 :(得分:20)

之前的答案很好,但我再次强调这一点。特别是,你说过

  

如果我理解正确,.net运行时将始终在我之后清理。

这只是部分正确。实际上, .NET only 为一个特定资源:主内存提供自动管理。所有其他资源都需要手动清理。 1)

奇怪的是,主存在几乎所有关于程序资源的讨论中都处于特殊状态。当然有一个很好的理由 - 主存储器通常是最稀缺的资源。但值得记住的还有其他类型的资源,也需要管理。


1)通常尝试的解决方案是将其他资源的生命周期与代码中的内存位置或标识符的生命周期相结合 - 因此存在终结器。

答案 2 :(得分:9)

只有在系统没有内存压力的情况下才会运行垃圾收集器,除非它确实需要释放一些内存。这意味着,您永远无法确定GC何时运行。

现在,想象一下你是一个数据库连接。如果你在GC之后清理GC,你可能会连接到数据库的时间比需要的时间长得多,导致奇怪的负载情况。在这种情况下,您希望实现IDisposable,以便用户可以调用Dispose()或使用using()来确保尽快关闭连接,而不必依赖可能在以后运行的GC。

通常,IDisposable是在任何使用非托管资源的类上实现的。

答案 3 :(得分:4)

  1. 之后垃圾收集器无法清理的东西
  2. 即使有可以清理的东西,也可以帮助它尽快清理

答案 4 :(得分:2)

真正的原因是因为.net垃圾收集不是为了收集非托管资源而设计的,因此这些资源的清理仍然掌握在开发人员手中。 此外,当对象超出范围时,不会自动调用对象终结器。它们在某个未确定的时间由GC调用。当他们被调用时,GC不会立即运行它,等待下一轮调用它,增加清理时间,当对象持有稀缺的非托管资源(例如文件)时,这不是一件好事或网络连接)。 输入一次性模式,开发人员可以在确定的时间(调用yourobject.Dispose()或using(...)语句时)手动释放稀缺资源。 请记住,你应该调用GC.SuppressFinalize(this);在您的dispose方法中告诉GC该对象是手动处理的,不应该最终确定。 我建议你看看K. Cwalina和B. Abrams的框架设计指南书。它解释了Disposable模式非常好。

祝你好运!

答案 5 :(得分:2)

简单的解释:

  • Dispose专为确定性处理非内存资源而设计,尤其是稀缺资源。例如,窗口句柄或数据库连接。
  • Finalize是为非确定性处理非内存资源而设计的,通常在没有调用Dispose的情况下作为后备。

实施Finalize方法的一些准则:

  • 仅对需要完成的对象实施Finalize,因为与Finalize方法相关的性能成本。
  • 如果您需要Finalize方法,请考虑实施IDisposable以允许您的类型的用户避免调用Finalize方法的成本。
  • 您的Finalize方法应该受到保护而不是公开。
  • 您的Finalize方法应该释放该类型拥有的所有外部资源,但它拥有的那些资源。它不应该引用任何其他资源。
  • CLR不会对调用Finalize方法的顺序做出任何保证。正如丹尼尔在评论中指出的那样,这意味着如果可能的话,Finalize方法不应该访问任何成员引用类型,因为它们可能(或可能有一天)拥有自己的终结器。
  • 不要直接在类型的基本类型之外的任何类型上调用Finalize方法。
  • 尽量避免在Finalize方法中出现任何未处理的异常,因为这会终止您的流程(在2.0或更高版本中)。
  • 避免在Finalizer方法中执行任何长时间运行的任务,因为这将阻止Finalizer线程并阻止执行其他Finalizer方法。

实施Dispose方法的一些准则:

  • 在封装显式需要释放的资源的类型上实现dispose设计模式。
  • 在具有一个或多个保留资源的派生类型的基类型上实现dispose设计模式,即使基类型没有。
  • 在实例上调用Dispose后,通过调用GC.SuppressFinalize方法阻止Finalize方法运行。此规则的唯一例外是极少数情况下必须在Finalize中完成工作,而Dispose未涵盖这些工作。
  • 不要以为会调用Dispose。如果未调用Dispose,则还应在Finalize方法中释放类型所拥有的非托管资源。
  • 当资源已经处理时,从此类型的实例方法(Dispose除外)抛出ObjectDisposedException。此规则不适用于Dispose方法,因为它应该可以多次调用而不会抛出异常。
  • 通过基本类型的层次结构传播对Dispose的调用。 Dispose方法应释放此对象持有的所有资源以及此对象拥有的任何对象。
  • 您应该考虑在调用Dispose方法后不允许对象可用。重新创建已经处置的对象是一种难以实现的模式。
  • 允许多次调用Dispose方法而不抛出异常。第一次通话后该方法无效。

答案 6 :(得分:1)

需要descructors和dispose方法的对象正在使用未受管理的资源。因此垃圾收集器无法清理这些资源,您必须自己完成。

查看IDisposable的MSDN文档; http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

该示例使用非托管处理程序 - IntPr。

答案 7 :(得分:0)

某些对象可能需要清理低级别的项目。比如需要关闭的硬件等等。

答案 8 :(得分:0)

主要用于非托管代码,以及与非托管代码的交互。 “纯”托管代码永远不需要终结器。另一方面,一次性使用只是一种方便的模式,可以在你完成任务后强行释放。

答案 9 :(得分:0)

.NET垃圾收集器知道如何在.NET运行时中处理托管对象。但Dispose模式(IDisposable)主要用于应用程序正在使用的非托管对象。

换句话说,.NET运行时不一定知道如何处理每种类型的设备或处理那些(关闭网络连接,文件句柄,图形设备等),所以使用IDisposable提供了一种说法“让我在一个类型中实现一些自己的清理”。看到这个实现,垃圾收集器可以调用Dispose()并确保清理托管堆之外的东西。

答案 10 :(得分:0)

有一些(很少)的情况下,当不再使用纯粹的托管对象时,可能需要执行特定的操作,我无法想出一个顶端的例子,但我有多年来看到了几种合法用途。但主要原因是清理对象可能正在使用的任何非托管资源。

因此,通常,除非使用非托管资源,否则不需要使用Dispose / Finalize模式。

答案 11 :(得分:0)

因为垃圾收集器无法收集托管环境未分配的内容。因此,需要以旧方式收集对导致内存分配的非托管API的任何调用。