【问题标题】:Minimal IDisposable implimenation for managed resources only仅针对托管资源的最小 IDisposable 实现
【发布时间】:2013-09-29 00:39:52
【问题描述】:

关于处置非托管资源的“标准完整”IDisposable 实现有很多信息 - 但实际上这种情况(非常)罕见(大多数资源已经被托管类包装)。这个问题的重点是 IDisposable 的最小实现,用于更常见的“仅托管资源”情况。

1:下面代码中IDisposable的最小实现是否正确,有问题吗?

2:是否有任何理由添加完整的标准IDisposable 实现(Dispose()Dispose(bool)Finalizer 等)而不是提供的最小实现?

3:在这种最小情况下,将Dispose 设为虚拟是否可行/明智(因为我们不提供Dispose(bool))?

4:如果这个最小实现被包含(在这种情况下无用的)终结器的完整标准实现替换 - 这会改变 GC 处理对象的方式吗?有什么缺点吗?

5:该示例包括 Timer 和事件处理程序,因为这些情况特别重要,不要错过,因为未能处理它们将使对象保持活动状态(Timer 的情况下为thiseventSource 在事件处理程序的情况),直到 GC 及时处理它们。还有其他类似的例子吗?

class A : IDisposable {
    private Timer timer;
    public A(MyEventSource eventSource) {
        eventSource += Handler
    }

    private void Handler(object source, EventArgs args) { ... }

    public virtual void Dispose() {
        timer.Dispose();
        if (eventSource != null)
           eventSource -= Handler;
    }
}

class B : A, IDisposable {
    private TcpClient tpcClient;

    public override void Dispose() {
        (tcpClient as IDispose).Dispose();
        base.Dispose();
    }   
}

参考:
MSDN
SO: When do I need to manage managed resources
SO: How to dispose managed resource in Dispose() method in C#
SO: Dispose() for cleaning up managed resources

【问题讨论】:

  • 是 1) 和 1)。当然看起来你已经知道了。
  • @HansPassant 我已经改写了这个问题。您的意见将不胜感激。

标签: c# dispose idisposable


【解决方案1】:
  1. 实现是正确的,没有问题,前提是没有派生类直接拥有非托管资源。

  2. 实施完整模式的一个很好的理由是“最小意外原则”。由于 MSDN 中没有描述这种更简单模式的权威文档,因此维护开发人员可能会有疑问 - 甚至您觉得有必要询问 StackOverflow :)

  3. 是的,在这种情况下,Dispose 可以是虚拟的。

  4. 如果 Dispose 已被调用并且被正确实现(即调用 GC.SuppressFinalize),则不必要的终结器的开销可以忽略不计

.NET Framework 本身之外的绝大多数IDisposable 类是IDisposable,因为它们拥有托管的IDisposable 资源。他们很少直接持有非托管资源 - 这只发生在使用 P/Invoke 访问未由 .NET Framework 公开的非托管资源时。

因此,推广这种更简单的模式可能有一个很好的论据:

  • 在使用非托管资源的极少数情况下,应将它们包装在实现终结器的密封 IDisposable 包装类中(如 SafeHandle)。因为它是密封的,所以这个类不需要完整的 IDisposable 模式。

  • 在所有其他情况下,绝大多数情况下,都可以使用您更简单的模式。

但除非并且直到微软或其他权威来源积极推广它,否则我将继续使用完整的IDisposable 模式。

【讨论】:

  • 这个。我也是使用完整模式的粉丝,但我可以看到 OP 的重点。无论如何,+1。
  • 一个很好的答案 - 谢谢。似乎有不成比例的关于未托管资源的完整 IDispose 实施的文档,正如您所说,这实际上非常罕见 - 关于这种情况下的密封包装器的优点。我只是想检查一下我对仅在托管资源的情况下完整实现减少到什么的理解是否正确。谢谢。
  • @Ricibob,我很乐意看到 Microsoft 通过 MSDN 提供一些非常明确的指导,包括当您有多个资源要处置以及其中一个 Dispose 方法抛出时该怎么做。您是否应该使用 try/catch 块来尝试处置尽可能多的您拥有的资源?恕我直言,您可能应该这样做,但是框架(System.ComponentModel.Container)中有一个示例不会这样做。
  • 是 Dispose 中的异常处理是另一个问题。上面还没有提到不必要的终结器的问题。我经常看到用于完整实现上述案例的终结器的标准实现 - 但我的理解是它是一个。不必要的和b。可以提供性能影响,因为添加终结器会影响 GC 处理对象的方式。在上述情况下添加无用的终结器是否有不利之处?
  • 我认为,如果 Dispose 已被调用并且被正确实现(即调用 GC.SuppressFinalize),那么不必要的终结器的开销可以忽略不计。
【解决方案2】:

另一种选择是重构您的代码以避免继承并使您的IDisposable 类密封。然后更简单的模式很容易证明是正确的,因为不再需要支持可能的继承的笨拙的旋转。我个人大部分时间都采用这种方法。在极少数情况下,我想让非密封类一次性使用,我只是遵循“标准”模式。培养这种方法的一个好处是它倾向于将您推向组合而不是继承,这通常使代码更易于维护和测试。

【讨论】:

  • 完全同意。我来自 C++ 背景——继承似乎是首选方法——但逐渐学习组合确实使代码更简单。
【解决方案3】:

我推荐的Dispose 模式是用于将非虚拟Dispose 实现链接到虚拟void Dispose(bool),最好是在以下内容之后:

int _disposed;
public bool Disposed { return _disposed != 0; }
void Dispose()
{
  if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0)
    Dispose(true);
  GC.SuppressFinalize(this); // In case our object holds references to *managed* resources
}

使用这种方法将确保Dispose(bool) 只被调用一次,即使多个线程尝试同时调用它。虽然这种同时处理的尝试很少见(*),但防范它们的成本很低;如果基类不执行上述操作,则每个派生类都必须有自己的冗余双重处置保护逻辑,并且可能还有一个冗余标志。

(*) 一些主要是单线程并使用阻塞 I/O 的通信类允许从任何线程上下文调用 Dispose 以取消阻塞其自己线程的 I/O 操作 [显然是 Dispose不能在该线程上调用,因为该线程在被阻塞时无法执行任何操作]。对于此类对象或封装它们的对象,完全有可能(而且并非不合理)让外部线程尝试Dispose 它们作为在它们将被处理的那一刻中止它们当前操作的一种手段他们的主线。同时Dispose 调用可能很少见,但它们的可能性并不表示任何“设计问题”,前提是Dispose 代码可以对一个调用起作用而忽略另一个。

【讨论】:

  • 就我个人而言,我发现这种模式对于绝大多数 IDisposable 类来说太过分了,因为多线程 Dispose 在术语上几乎是矛盾的。添加联锁增量在执行时间方面很便宜,但在维护程序员想知道它为什么存在的时间方面可能并不便宜。我也找不到您的假设示例令人信服:恕我直言,公开一个 API 以允许其他线程中断阻塞 I/O 操作不需要 Dispose 模式 - 可以说是一个显式 API,如“InterruptBlockingIO”更好,并且被阻塞的线程可以进行处置
  • 一个 volatile bool isDisposed 更简单?同样在严格管理的资源情况下(大多数情况下!),不需要进行已经释放的检查 - 因为我们所做的只是在成员或基上调用 Dispose - 并且那些 Dispose 方法应该已经处理了双重调用。
  • @Ricibob:使用Interlocked.Exchange 将100% 防止双重调用;不稳定的bool 不会。至于这种保护的必要性,虽然我同意没有它通常可以逃脱,但它很便宜,而且我建议提供这种保护通常比证明不需要它更容易。例如,假设Foo 拥有两个托管的“双重处置受保护”资源,并且它们必须按顺序处置。线程 1 调用 Foo.Dispose 并在线程 2 调用 Foo.Dispose 时开始处理第一个对象。
  • @Ricibob:线程 2 可以在线程 1 完成处理第一个对象之前处理第二个对象。这种情况不太可能出现,但这并不意味着很容易证明它们不会。添加Interlocked.Exchange 可以更容易地证明Dispose 的所有部分都将以正确的顺序完成,尽管您让我认为另一种模式可能更好,因为代码可能期望与@ 相关的所有条件987654338@在它返回之前申请。
猜你喜欢
  • 2011-12-20
  • 2012-03-28
  • 1970-01-01
  • 2012-05-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-17
  • 1970-01-01
相关资源
最近更新 更多