ChatGPT解决这个技术问题 Extra ChatGPT

既然 .NET 有一个垃圾收集器,为什么我们需要终结器/析构器/dispose-pattern?

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

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


D
Dinah

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

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# 不同,是确定性的。


IMO这是最好的答案。其中最重要的部分——以及我们使用一次性语法的原因——是提供稀缺资源的确定性释放。很棒的帖子。
很好的答案,尽管终结器不会在对象生命周期结束时自动运行。否则我们不需要一次性模式。当 GC 确定需要运行它们时(谁知道何时),它们会被调用。
只是为了记录。终结器不能保证运行。它们由专用线程按顺序执行,因此如果终结器进入死锁,则不会运行其他终结器(并且内存会泄漏)。显然终结器不应该阻塞,但我只是说有一些警告。
这可能就是为什么有传言称框架可能开始使用 ThreadPool 来执行终结器的原因。
Eric Lippert 最近写了一篇关于析构函数/终结函数的区别的博客blogs.msdn.com/ericlippert/archive/2010/01/21/…
K
Konrad Rudolph

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

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

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

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

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


您可以通过提及它是错误的解决方案来改进该脚注!可替代和不可替代的商品必须以不同的方式处理。
Earwicker:我同意你的看法。但是,由于我不知道任何实现可行替代方案的语言,我真的不知道什么会更好。特别是因为无论如何每个资源都绑定到一个标识符,并且该标识符与其内存具有相同的生命周期。
C# 的 using 关键字是一个可行的替代方案:当执行离开代码块时,就该释放资源了。对于不可替代的资源,这比将它们的生命周期与诸如释放内存之类的可替代资源联系起来更可取。
@Earwicker:这是我不再同意的地方。 using 有利有弊,但我不确定前者是否胜过后者。当然,这取决于应用程序,但在我编写的几乎每个程序中,非托管资源管理都是至关重要的部分,而 C++ 让我的生活变得更加轻松。
您可能想查看 C++/CLI 以了解析构函数如何完美地映射到 IDisposable。我同意 C++/CLI 的支持更完整,因为它会自动传播对成员对象、继承对象等的 Dipose 调用,其中 C# 的使用仅重现 C++ 如何处理堆栈上的对象。
M
Michael Stum

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

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

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


INCORRECT => "垃圾收集器只会在系统没有内存压力的情况下运行,除非它真的需要释放一些内存。"实际上,这种说法是不正确的。 GC 在 3 种情况下运行(其中只有一种是确定性的):1)当请求内存分配并且已超过该对象生成的当前段大小时,2)系统处于内存压力(OS)下,3)正在卸载 AppDomain
INCORRECT => "一般来说,IDisposable 是在任何使用非托管资源的类上实现的。"这种说法也是不正确的。 IDisposable 模式应该在类成员实现 IDisposable 的任何时候实现,并且总是在您处理非托管资源时实现
o
orip

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


R
Ricardo Villamil

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

祝你好运!


H
HTTP 410

简单的解释:

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 方法而不引发异常。该方法在第一次调用后应该什么都不做。


r
rjzii

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

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

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


GC可以清理资源,你只是不知道什么时候。
GC 通常会清理资源,但并非总是如此。例如,在 System.DirectoryServices.SearchResultCollection 的 MSDN 文档中:“由于实现限制,SearchResultCollection 类在垃圾回收时无法释放其所有非托管资源”
B
Brian Knoblauch

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


J
Jeff Mc

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


S
Stephen Martin

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

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


Q
Quibblesome

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


J
Jeff Donnici

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

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


谢谢...通过将“.NET 堆栈/堆外部”更改为“托管堆”来澄清。