【问题标题】:Why destructor is so not enough to implement IDisposable?为什么析构函数不足以实现 IDisposable?
【发布时间】:2014-02-11 03:00:23
【问题描述】:

假设我们正在使用一些非托管资源。

最常见的做法是:

//With IDisposable
using (MemoryStream memoryStream = new MemoryStream())
{
   //Operate with memory stream
}

但是我们不能写以下吗?

//With destructor called at the end of a block
{
    MemoryStream memoryStream = new MemoryStream();
    //Operate with memory stream
}
  • IDisposable的主要目标是什么?

  • 它只是终结逻辑分离的设计模式吗?

  • 例如,Java 是否提供类似的功能?

【问题讨论】:

  • 您可以在 Java 7 中的 AutoCloseables 上使用 try-with-resources 块:try (AutoCloseable resource = ...) { ... }。在 Java 中,析构函数不能用于此目的,因为它们可以在 GC 线程上的任意时间被调用。
  • Java 提供了终结器和类似于using 的东西。终结器在 Java 中和在 C# 中一样不可靠,它们的唯一用途是为了以防万一。
  • 在块的末尾不调用析构函数。它在不确定的时间被调用,由 GC 决定。即使在 C++ 中,如果您这样做,也不会在块的末尾调用析构函数。您在堆上分配一个对象,它的生命周期不限于它的范围。另一方面,Dispose IS 在 using 块的末尾调用。
  • 您假设析构函数将在块的末尾被调用。你检查过这个假设吗?

标签: c# destructor idisposable


【解决方案1】:
  • IDisposable的主要目标是什么?

  • 它只是终结逻辑分离的设计模式吗?

它的目标只是成为调用该逻辑的标准接口。 recommended implementation 将手动处理 (Dispose(true)) 与从终结器 (Dispose(false)) 调用的 Dispose 中分离,只是为了提高效率——从终结器中处理内部字段是多余的,因为由于无法手动调用终结器,我们知道那时他们一定已经被收集了。

需要手动处理,因为垃圾收集不是即时的(而且您不能单独强制收集单个对象)。这不仅仅是一个效率问题,因为非托管资源可能不支持多次访问,因此如果第一个对象尚未收集,则稍后访问它的另一个对象将失败。甚至不能保证集合,因为正如Ricibob 所示,将代码放在显式范围内不会阻止外部对象获取自己的引用。

  • 例如,Java 是否提供类似的功能?

using 只是一个自动实现的“try ... finally”块,它调用预定义的方法。

Java 将此功能包含为extension of the try statement itself。这与using 的工作方式相同,但它还允许您添加自己的catchfinally 块,而无需将其包装在额外的try 块中。

Pythoncontext managers,这是一个更灵活的版本。上下文管理器可以定义特定的异常处理以及finally,并且可能返回与传入的对象不同的对象——也就是说,这是可能的:

with CustomDisposer(MemoryStream()) as memoryStream:

CustomDisposer 对象负责 dispose 实现,但它返回 MemoryStream 作为要分配给 memoryStream 变量的资源。

Ruby 有一个 yield 语句,它允许一个函数 wrap a code block 并可选地为块提供一个参数,因此您可以通过将给定对象传递给块然后调用来实现这一点在ensure 中处理(finally 等效):

def using(o)
    yield o
ensure
    o.dispose
end

using MemoryStream.new do |memoryStream|
    #Operate with memory stream
end

当然,因为这是函数定义的一部分,所以不需要专门的using函数——例如可以直接在MemoryStream.Open方法中实现。

【讨论】:

  • 我不建议在 .net 2.0 或更高版本上实现。 SafeHandle 基于类通常是一个更好的主意。
【解决方案2】:

析构函数不会在块结束时调用,而是在 GC 销毁对象时调用。您无法控制何时发生这种情况。有时你需要那种控制。这就是IDisposable 发挥作用的时候。

【讨论】:

    【解决方案3】:

    考虑:

    var sc = new StreamConsumer();
    {
        var memoryStream = new MemoryStream();
        //Operate with memory stream
        sc.Stream = memoryStream;
    }
    sc.DoStuffWithStream();
    

    这里是 GC 工作来确定我们何时真正完成了 memoryStream 并且它可以被销毁。这并不总是直截了当(正如 C++ 开发人员所熟知的......)我们不知道 GC 什么时候会这样做 - 它在时间上不是确定性的。

    使用/IDisposable 不是关于对象销毁,而是关于以及时确定的方式控制对象对资源的访问。当我们应用 using 时说,在此语句之外,我知道资源(文件、内存映射文件、套接字等)是免费的并且可供其他对象使用。如果我尝试访问已处置的对象(在其使用范围之外),因为有其他对象持有它的引用 - 那么我会得到一个异常。

    【讨论】:

      【解决方案4】:

      同样重要的是要注意终结器(析构函数)实际上根本不能保证运行。通常在实现终结器时,它所做的只是调用 Dispose 来解决消费者忘记将对象放入 using 块中的可能性。如果您怀疑对象未释放时内存泄漏,则可以实施此模式。

      微软甚至给这种做法起了一个名字:http://msdn.microsoft.com/en-us/library/b1yfkh5e(v=vs.110).aspx

      要点是:在 C# 中,您几乎应该总是使用 IDisposable。

      【讨论】:

        【解决方案5】:

        GC 的基本目的是确保对任何对象的引用永远不会变成对该对象的引用以外的任何东西。因此,如果它可以看到任何可能的方法来访问对它们的引用,它就必须保留对象。因为存在强根引用的对象通常是“有用的”,而不存在强根引用的对象通常不是“有用的”,垃圾收集器也可以在一定程度上识别出无用的对象,并且应该具有对它们的所有引用被淘汰了。

        不幸的是,即使 GC 响应完美并且可以立即识别对任何给定对象的最后一个强引用何时消失,但这并不意味着它可以可靠地识别无用的对象。如果 X 持有对 Y 的强引用,这通常意味着如果某物对 X 感兴趣,那么某物将对 Y 感兴趣。然而,在某些情况下,这将意味着 Y 有兴趣从 X 接收某种通知。假设 X 已注册从静态计时器计时处理程序接收计时器计时事件,并且每次从 X 接收到事件并且某些条件为真时,Y 都会增加一个 Int64 计数器。引用该计数器的其他对象可以检查计数器的状态。只要存在对 Y 的引用 除了 X 持有的引用之外,计时器事件就应该继续触发,并且 X 应该继续更新 Y 的计数器。一旦对 Y 的所有其他引用不再存在,Y 的计数器将无用,Y 更新它的努力也将无用。这反过来会使 X 的通知无用,这将使 X 接收到的静态计时器事件无用。使用的定时器资源应该被释放,并且X从静态处理程序中分离;然后 X 和 Y 将不复存在。除了一个问题之外,一切都非常好和有序:即使 X 的有用性取决于 Y 的有用性,而计时器的有用性取决于 Y 的有用性,对象引用指向另一种方式。虽然可以安排一些事情,以便在 Y 被放弃时释放计时器,但这样做会增加在使用计时器时需要运行的代码的大量开销。

        【讨论】:

          猜你喜欢
          • 2018-10-12
          • 2016-02-13
          • 2012-07-04
          • 2012-01-19
          • 1970-01-01
          • 1970-01-01
          • 2014-02-02
          • 2020-03-23
          • 1970-01-01
          相关资源
          最近更新 更多