2

托管代码的主要优点之一是内置内存管理。您不需要跟踪指针、缓冲区大小、释放您完成的内存等,托管方面会为您完成这些。

那么为什么我们有一个IDisposable接口呢? MSDN 说接口是处理非托管资源,如窗口句柄、文件等。但为什么要求我显式调用Dispose方法(或使用Using)?

  1. 为什么当对象超出范围并Dispose自动调用时,CLR 不能跟踪?
Public Function DoSomething() As String
    Dim reader As New StreamReader("myfile.txt")
    Dim txtFromFile As String = reader.ReadToEnd()

    Return txtFromFile '<==== reader goes out of scope after this line, so call Dispose automatically
End Function
  1. 至少,为什么垃圾收集器最终不会找到它并调用Dispose

我错过了什么?

编辑

有几个人(这里和其他建议的答案Using)建议垃圾收集不够好,因为 GC最终只能收集IDisposable​​. 我不明白为什么该论点区分 anIDisposable和 .NET 中的任何其他对象。在您说IDisposable对象更占用资源之前,请考虑:

  1. 上面的MSDNIDisposable适用于非托管对象,无论它们的资源要求如何
  2. 我见过一些资源非常密集的 .NET 对象(System.Web.UI.PageSystem.Data.Objects.ObjectContext怎么样)。
4

5 回答 5

2

正如我在评论中所说,CLR 不会跟踪对象何时超出范围。

让我们举个例子:

Public Function DoSomething() As String
    Dim reader As New StreamReader("myfile.txt")
    Dim txtFromFile As String = reader.ReadToEnd()

    Return txtFromFile '<==== reader goes out of scope after this line, so call Dispose automatically
End Function

它实际上需要分析方法的整个主体,检查您是否已将对该阅读器的引用传递给另一个方法,或者将引用存储在例如字段中。

然后它需要确定该其他方法是否已将引用存储在某处,或者是否调用了其他方法等。

然后它必须推断,如果引用存储在其他任何地方,其意图是否是其他东西稍后会使用该引用并期望在那里找到一个未处置的实例。

将其与您的知识进行比较。您知道(希望)您是否在其他地方存储了引用,或者将引用传递给其他已“获得”此一次性用品的“所有权”。如果您知道这些都没有发生,您可以将此知识传达给编译器 - 通过添加一个Using块。


或者无论如何,垃圾收集器最终不会到达并调用Dispose吗?

如果 a) 对象直接“包含”非托管资源,并且 b) 实现该对象的人遵循最佳实践,那么他们应该在该对象上实现一个终结器,该终结器将对非托管资源执行清理(通常通过调用之间共享的方法)终结器和Dispose)。

但是,您不知道一次垃圾回收何时发生。同时,您可能会拒绝其他程序访问相同的非托管资源 - 甚至是您自己程序的另一部分。您应该将非托管资源视为稀缺资源。如果有人已实现,他们希望您在您知道不再需要访问该资源时Dispose调用它(显式或通过)。Using

于 2012-11-21T09:59:34.530 回答
1

当对象超出范围时,CLR 不会跟踪。垃圾收集器的重点是它最终会收集死对象。但关键词是“最终”。您无法保证它何时发生,因此如果您需要它在特定时间发生,您必须做一些事情来实现它。这是什么Dispose()using是为了什么。

通常,CLR无法跟踪对象何时超出范围,因为虽然它可能不再存在于创建它的范围内,但引用可能已传递给另一个范围内的另一个函数,可能在另一个线程上。

当你创建一个对象时,你只会得到一个对它的引用。像其他任何参考一样。该对象不在当前范围内,它与创建它的站点没有特殊附件。

这与 C++ 中的情况不同,在 C++ 中创建对象时,它就存在于其中,并且当您离开特定范围时将调用析构函数(并且如果对该对象的引用存在于其他地方,则运气不好)

所以在这种情况下,您实际上必须更加小心并在垃圾收集环境中编写更多代码。您不能仅仅依赖资源的生命周期受其声明的范围的限制,因此您必须明确说明何时应该处置它。

于 2012-11-21T10:06:26.703 回答
1

C++ 语言旨在准确跟踪事物何时进入和超出范围。在对象具有明确可识别的“所有者”的情况下,它非常擅长这一点。不幸的是,在许多情况下,对象本身并没有特定的所有者。例如,如果某个对象生成一个包含字符“Hello”的字符串并将其传递给存储对它的引用的Foo其他对象,则既不BarFooBar将有任何方式知道其他对象是否以及何时不再需要该字符串。C++ 可以通过让每个字符串关联一个计数器来处理这种情况,使用任何方法创建一个引用递增该计数器,并使用任何放弃引用的方法递减它,如果计数器达到零,则删除该对象。不幸的是,要求方法在每次传递对对象的引用时增加和减少计数器可能会很昂贵,尤其是在多处理器机器上(因为有必要确保如果例如计数等于 3 并且两个处理器同时创建对对象,一个会将计数从 3 增加到 4,另一个将计数从 4 增加到 5,而不是让两个处理器都看到计数是 3,然后都将其设置为 4,这将是一场灾难)。

垃圾收集系统通过简单地让程序绕过引用来避免这个问题,而不必担心跟踪对对象的最后一个引用何时被放弃或覆盖。任何时候系统决定它应该尝试重用一些内存,它可以暂时冻结所有内容,检查哪些对象仍然具有对它们的实时引用,并释放不再需要的对象使用的任何内存。当系统决定启动垃圾收集周期时,所有进程都必须同步(不同的系统在必须冻结所有内容的时间上有所不同),但其成本可能远低于任何时候要求处理器间同步的成本例程将引用传递给另一个。

垃圾收集系统可以很好地管理内存。释放对象使用的内存将无济于事,直到如果内存被释放,内存实际上将用于某些事情。不需要使用太多内存的应用程序可能会在不执行任何垃圾收集的情况下运行任意长时间这一事实是一件好事,因为当没有其他东西需要内存时,将对象不必要地保留在内存中并没有什么坏处无论如何。不幸的是,应用程序没有使用太多内存这一事实并不意味着没有任何废弃的对象可以独占使用内存以外的资源(例如,为独占访问而打开的文件之类的东西)并阻止其他对象- 成为这些资源的用户,避免访问它们。

于 2012-11-23T23:34:33.603 回答
1

只是为了将其他答案中所有令人惊奇的有用信息汇总到一个地方,我认为这是理解为什么.NET需要IDisposable.

资源可用性

  1. 垃圾收集方案(即“托管代码”)是为管理内存而设计的并且非常擅长管理内存。
  2. 还有其他类型的系统资源,例如:文件句柄、数据库连接、套接字、窗口、进程等。
  3. GC 的一个关键特性是它会留下废弃的内存,直到有其他东西需要内存,而不是尽快清理它。这对内存有好处(谁在乎它不使用时的作用),但对其他资源却不是那么好。它们取决于在实际可用时被列为可用。
  4. 如果没有IDisposable,非内存资源(文件等)将不可用,直到 GC 碰巧最终确定了首先获取它们的对象,即当系统需要该对象的内存空间时。这可能是 a) 很长一段时间,或 b) 永远不会。

定稿

  1. GC 以不确定的顺序最终确定对象。更糟糕的是,它将对象终结的托管和非托管部分分开。
  2. 这意味着,GC 可以在非托管部分之前完成对象的托管部分。如果非托管尝试回调托管...

代码结构

  1. 编译器没有好的方法可以知道对象何时“被遗弃”。
  2. 块为变量提供Using了专门的作用域以通知编译器,“在这个块结束后,我将放弃这个对象”。
  3. 这将管理引用的责任转移到了程序员身上,而不是 CLR(通常在哪里)。如果你从一个Using块中传递一个资源,你很可能会得到一个NullReferenceException
  4. 由于上面讨论的原因,这个负担确实需要落在程序员身上。
于 2012-11-24T17:40:27.537 回答
0

也许如果您有一个长期运行的函数,或者如果您已经使用外部资源在长期存在的类中初始化了一个字段,那么等到变量/字段超出范围可能是不可取的。

此外,如果一个静态字段已经像这样初始化,它永远不会超出范围。

于 2012-11-21T07:44:19.857 回答