68

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

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

4

12 回答 12

94

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

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# 析构函数称为“终结器”,否则它们往往会将人们与 C++ 析构函数混淆,这与 C# 不同,是确定性的。

于 2008-12-01T18:49:52.970 回答
21

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

如果我理解正确,.net 运行时总是会在我之后清理。

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

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


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

于 2008-12-01T19:08:00.267 回答
9

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

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

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

于 2008-12-01T18:51:04.577 回答
4
  1. 有些东西垃圾收集器在你之后无法清理
  2. 即使有它可以清理的东西,你也可以帮助它尽快清理
于 2008-12-01T18:55:17.920 回答
2

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

祝你好运!

于 2008-12-01T19:01:15.490 回答
2

简单的解释:

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

实现 Finalize 方法的一些准则:

  • 仅在需要终结的对象上实现终结,因为终结方法会带来性能成本。
  • 如果您需要 Finalize 方法,请考虑实现 IDisposable 以允许您的类型的用户避免调用 Finalize 方法的成本。
  • 您的 Finalize 方法应该受到保护而不是公开。
  • 您的 Finalize 方法应该释放该类型拥有的任何外部资源,但只能释放它拥有的那些。它不应引用任何其他资源。
  • CLR 不保证调用 Finalize 方法的顺序。正如 Daniel 在他的评论中指出的那样,这意味着 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 方法而不引发异常。该方法在第一次调用后应该什么都不做。
于 2008-12-01T19:24:58.157 回答
1

需要析构函数和处置方法的对象正在使用非托管资源。所以垃圾收集器不能清理那些资源,你必须自己做。

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

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

于 2008-12-01T18:50:31.473 回答
0

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

于 2008-12-01T18:49:44.703 回答
0

主要针对非托管代码,以及与非托管代码的交互。“纯”托管代码永远不需要终结器。另一方面,Disposable 只是一种方便的模式,可以在您完成后强制释放某些东西。

于 2008-12-01T18:50:38.837 回答
0

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

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

于 2008-12-01T18:51:36.057 回答
0

在少数(非常少)情况下,当不再使用纯托管对象时,可能需要执行特定操作,我想不出一个例子,但我见过几个多年来的合法用途。但主要原因是清理对象可能正在使用的任何非托管资源。

因此,一般来说,除非您使用非托管资源,否则您不需要使用 Dispose/Finalize 模式。

于 2008-12-01T18:53:32.993 回答
0

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

于 2008-12-01T18:56:06.407 回答