仅针对托管资源的最小IDisposable实现

12

有很多关于“标准完整”IDisposable实现释放非托管资源的信息,但实际上这种情况非常罕见(大多数资源已经由托管类封装)。此问题侧重于针对更常见的“仅托管资源”的最小化实现。

1:以下代码中的的最小实现是否正确,是否存在问题?

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

3:在这种最小情况下,使Dispose虚拟化是否可以(因为我们没有提供Dispose(bool))?

4:如果将这种最小实现替换为包括(在这种情况下无用的)终结器的完整标准实现,这是否会改变GC处理对象的方式?是否存在任何缺点?

5:示例包括Timer和事件处理程序,因为这些情况特别重要,不容忽视,因为未能处理它们将使对象保持活动状态(在计时器的情况下为this,在事件处理程序的情况下为eventSource),直到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: 当我需要管理托管资源时
SO: 如何在C#中的Dispose()方法中处理托管资源
SO: Dispose()用于清理托管资源


1
这是1)和1)。看起来你已经知道了。 - Hans Passant
@HansPassant,我已经重新措辞了问题。欢迎您的意见。 - Ricibob
3个回答

8
  1. 如果没有派生类直接拥有非托管资源,那么实现是正确的,没有问题。

  2. 实现完整模式的一个好理由是"最小惊奇原则"。由于MSDN中没有描述这个更简单模式的权威文档,后续的维护开发人员可能会有疑虑 - 就像你曾经需要问StackOverflow一样:)

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

  4. 如果Dispose已被调用并且已正确实现(i.e. 调用了GC.SuppressFinalize),则不必要的finalizer的开销是可以忽略不计的。

在.NET Framework之外的绝大多数IDisposable类都是因为它们拥有管理的IDisposable资源而需要实现IDisposable。但是,只有在使用P/Invoke访问.NET Framework未公开的非托管资源时,它们才直接持有非托管资源,这种情况很少见。

因此,可能有一个很好的理由来提倡这个更简单的模式:

  • 在罕见的情况下使用非托管资源时,应该将它们包装在一个密封的IDisposable包装类中,并实现一个finalizer(如SafeHandle)。由于它是密封的,所以这个类不需要完整的IDisposable模式。

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

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


1
非常好的答案 - 谢谢。似乎有关于完整IDispose实现非托管资源的文档比例不成比例,而正如您所说,这实际上非常罕见 - 对于这种情况使用密封包装器是个好主意。我只是想确认一下,在仅涉及托管资源的情况下,完整实现的含义是否正确。谢谢。 - Ricibob
@Ricibob,我个人非常希望微软能够通过MSDN提供一些非常明确的指导,包括当您有多个资源需要处理且其中一个Dispose方法抛出异常时该怎么办。您应该使用try/catch块尝试处理尽可能多的拥有资源吗?在我看来,您可能应该这样做,但是在框架(System.ComponentModel.Container)中有一个不会这样做的示例。 - Joe
是的,在Dispose中处理异常是另一个问题。此外,上面还没有提到不必要的终结器问题。我经常看到针对上述情况的标准终结器实现 - 但我的理解是a.它是不必要的,b.添加终结器会影响GC处理对象的方式,从而可能导致性能下降。在上述情况下添加无用的终结器是否有缺点? - Ricibob
如果Dispose已经被调用并且正确实现(即调用GC.SuppressFinalize),那么我认为不必要的finalizer的开销可以忽略不计。 - Joe
@supercat,我同意finalizer会增加一些成本,但我认为这个成本几乎总是可以忽略不计的。 - Joe
显示剩余6条评论

2
另一种选择是重构您的代码,避免继承并将您的IDisposable类标记为sealed。然后,更简单的模式就容易被证明,因为不再需要支持可能的继承的笨拙变通方法。个人大多数情况下采用这种方法;在我想让一个非sealed类成为可处理的情况下,我只需遵循“标准”模式即可。培养这种方法的好处之一是它倾向于推动您朝着组合而不是继承的方向发展,这通常使代码更易于维护和测试。

完全同意。我来自C++背景,那里继承似乎是首选方法,但逐渐学习组合确实使代码更简单。 - Ricibob

0

我推荐的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代码可以对一个调用进行操作并忽略另一个调用。


1
个人而言,我认为对于绝大多数IDisposable类来说,这种模式过度了,因为在多线程Dispose方面,几乎是自相矛盾的。添加交错增量在执行时间方面是便宜的,但也许对于维护程序员花费的时间来说并不便宜,他们会想知道为什么要这样做。我也不认为你的假设例子有说服力:在我看来,公开API以允许其他线程中断阻塞的I/O操作并不需要Dispose模式-可以说明确的API,如“InterruptBlockingIO”更好,而被阻塞的线程可以进行Dispose。 - Joe
一个 volatile bool 的 isDisposed 更简单吗?而且在严格管理资源的情况下(大多数情况!),没有必要进行已释放检查 - 因为我们所做的只是调用成员或基类上的 Dispose - 而那些 Dispose 方法应该已经处理了双重调用。 - Ricibob
@Ricibob:使用Interlocked.Exchange将100%防止双重调用;而一个易失的bool不会。至于这种保护的必要性,虽然我同意通常可以没有它,但它很便宜,我建议通常提供这种保护比证明它不需要更容易。例如,假设Foo持有两个托管的“双重释放保护”资源,并且它们必须按顺序处理。线程1调用Foo.Dispose并开始处理第一个对象时,线程2调用Foo.Dispose - supercat
@Ricibob:Thread 2在Thread 1完成第一个对象的处理之前处理第二个对象是可能的。这种情况不太可能出现,但这并不意味着很容易证明它们不会发生。添加“Interlocked.Exchange”使得更容易证明所有“Dispose”的部分将按正确的顺序完成,尽管您让我想到另一种模式可能更好,因为代码可能希望在返回之前应用于“Dispose”的所有条件。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接