由于.NET具有垃圾回收器,为什么我们需要终结器/析构函数/处置模式?


67

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

既然是这种情况,那么为什么某些对象需要具有析构函数或dispose方法?当不再引用它们时,运行时是否会清理它们?

Answers:


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#析构函数称为“ finalizers”,因为否则它们会使人与C ++析构函数相混淆,而C ++析构函数是确定性的。


4
IMO这是最好的答案。其中最重要的部分以及我们为什么要使用一次性语法,是为了确定性释放稀缺资源。很棒的帖子。
罗布

1
好的答案是,尽管终结器不会在对象寿命结束时自动运行。否则,我们将不需要一次性模式。当GC确定需要运行它们时,它们将被调用(谁知道何时运行)。
里卡多·维拉米尔

1
仅作记录。终结器不能保证运行。它们由专用线程顺序执行,因此,如果终结器进入死锁状态,则其他终结器将不运行(并且内存将泄漏)。显然,终结器不应阻止,但我只是说有一些警告。
布赖恩·拉斯穆森

这可能就是为什么有谣言称该框架可能开始使用ThreadPool执行终结器的原因。
Yona's

1
埃里克利珀最近在博客析构函数之间的区别/ finaliers blogs.msdn.com/ericlippert/archive/2010/01/21/...
伊恩

21

先前的答案很好,但让我再次在这里强调重点。特别是你说

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

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

奇怪的是,在几乎所有有关程序资源的讨论中,主内存都具有特殊的地位。当然,这有一个很好的理由–主内存通常是最稀缺的资源。但值得记住的是,还有其他类型的资源也需要管理。


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


1
您可以通过提及这是错误的解决方案来改进该脚注!可替代商品和不可替代商品的处理方式必须不同。
Daniel Earwicker

Earwicker:我同意你的看法。但是,由于我不知道有什么语言可以实现可行的替代方案,所以我真的不知道会有什么更好的选择。特别是因为无论如何每个资源都绑定到一个标识符,并且该标识符与其内存具有相同的生存期。
康拉德·鲁道夫

C#的using关键字是一个可行的选择:当执行离开代码块时,该释放资源了。对于不可替代资源而言,这比将其生命周期与诸如释放的内存之类的可替代事物绑定在一起更可取。
Daniel Earwicker

@Earwicker:这是我不再同意的地方。using具有优缺点,但我不确定前者是否胜过后者。当然,这取决于应用程序,但是在我编写的几乎每个程序中,非托管资源管理都是至关重要的部分,C ++在这里使我的生活变得更加轻松。
康拉德·鲁道夫

您可能想看一下C ++ / CLI,以了解析构函数如何完美地映射到IDisposable上。我同意C ++ / CLI的支持会更加完善,因为它会自动将Dipose调用传播给成员对象,继承的对象等,其中C#仅使用可重现C ++处理堆栈中对象的方式。
Daniel Earwicker

9

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

现在,假设您是数据库连接。如果您在之后清理GC,则连接到数据库的时间可能会超出所需的时间,从而导致奇怪的负载情况。在这种情况下,您希望实现IDisposable,以便用户可以调用Dispose()或使用using()来确保连接尽快关闭,而不必依赖GC,后者可能会在以后运行。

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


2
不正确=>“除非系统确实需要释放一些内存,否则垃圾收集器只会在系统没有内存压力的情况下运行。” 实际上,这个说法是不正确的。GC在3种情况下运行(其中只有一种是确定性的):1)当请求内存分配并且超出了该对象生成的当前段大小时,2)系统处于内存压力(OS)下,3)AppDomain正在卸载
Dave Black

1
“ INCORRECT =>”通常,IDisposable是在任何使用非托管资源的类上实现的。这个说法也不正确。当您在处理非托管资源时,只要类成员实现IDisposableable且始终都应实现IDisposable模式
Dave Black

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

2

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

祝好运!


2

简单的解释:

  • 处置旨在确定性处置非内存资源,尤其是稀缺资源。例如,窗口句柄或数据库连接。
  • Finalize设计用于非确定性地处置非内存资源,通常在不调用Dispose时作为后援。

实施Finalize方法的一些准则:

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

有关实现Dispose方法的一些准则:

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

1

需要描述符和处理方法的对象正在使用非托管资源。因此,垃圾收集器无法清理这些资源,因此您必须自己执行此操作。

查看IDisposable的MSDN文档。http://msdn.microsoft.com/zh-CN/library/system.idisposable.aspx

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


GC可以清理资源,您只是不知道什么时候。
Sunny Milenov

2
GC CAN通常会清理资源,但并非总是如此。例如,在System.DirectoryServices.SearchResultCollection的MSDN文档中:“由于实施限制,当进行垃圾回收时,SearchResultCollection类无法释放其所有非托管资源”
Joe


0

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


0

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

因此,通常,除非您使用的是非托管资源,否则您无需使用Dispose / Finalize模式。


0

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


0

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

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


谢谢...通过将“ .NET堆栈/堆外部”更改为“托管堆”进行了澄清。
杰夫·唐尼西
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.