在托管环境中,为什么我们需要IDisposable

时间:2012-11-21 07:13:53

标签: .net garbage-collection idisposable

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

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

  1. 为什么CLR在对象超出范围时无法跟踪并自动调用Dispose
  2. 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
    2. 我错过了什么?

      修改

      有些人(这里和其他建议Using的答案)建议垃圾收集不够好,因为GC只有最终才能收集IDisposable 。我不明白为什么这个参数区分了IDisposable和.NET中的任何其他对象。在您说IDisposable个对象资源更加密集之前,请考虑:

      1. 上面的MSDN表示IDisposable适用于非托管对象,无论其资源要求如何
      2. 我见过一些非常耗费资源的.NET对象(System.Web.UI.PageSystem.Data.Objects.ObjectContext怎么样)。

5 个答案:

答案 0 :(得分: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)调用它。

答案 1 :(得分:1)

CLR不会跟踪对象何时超出范围。垃圾收集器中的一点是最终会收集死对象。但关键字是“最终”。您无法保证何时发生,所以如果您需要在某个特定点发生,那么您必须采取措施才能实现。这是Dispose()using的用途。

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

创建对象时,您只需获得对象的引用。像任何其他参考。该对象不在当前范围内,它对创建它的站点没有特殊附件。

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

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

答案 2 :(得分:1)

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

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

垃圾收集系统可以很好地管理内存。释放对象使用的内存将无济于事,直到内存实际用于某些内容(如果它被释放)为止。不需要使用大量内存的应用程序可能会在没有执行任何垃圾收集的情况下进行任意长时间这一事实是一件好事,因为当没有其他需要内存的东西时,不必要地将对象留在内存中是没有害处的无论如何。不幸的是,应用程序没有使用太多内存这一事实并不意味着没有任何放弃的对象拥有除内存之外的资源的独占使用(例如,打开以供独占访问的文件之类的东西)并且正在阻止其他内存这些资源的用户访问它们。因此,有必要对不再需要的对象进行系统的通知,以确保获得专有资源的对象不会过长。

答案 3 :(得分:1)

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

资源可用性

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

    1. GC以非确定性顺序完成对象。更糟糕的是,它会分割对象最终化的托管和非托管部分。
    2. 这意味着,GC可能会在非托管部分之前完成对象的托管部分。如果非托管尝试回调托管...
    3. 代码结构

      1. 编译器没有办法知道某个对象何时被“放弃”。
      2. Using块为变量提供专门的范围,以通知编译器,“在此块结束后,我放弃了此对象”。
      3. 这将移动管理程序员上的引用而不是CLR(通常是它)的责任。如果您从Using块中传出资源,则可能会获得NullReferenceException
      4. 由于上面讨论的原因,这个负担确实需要在程序员身上。

答案 4 :(得分:0)

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

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