如何处理从Dispose抛出的异常?

25

最近,我在研究一些关于对象未处理的棘手错误。

我在代码中发现了一些模式。有人报告说一些m_foo没有被处理,而似乎所有SomeClass的实例都已经被处理了。

public class SomeClass: IDisposable
{
    void Dispose()
    {
       if (m_foo != null)
       {
          m_foo.Dispose();
       }
       if (m_bar != null)
       {
          m_bar.Dispose();
       }   
    }

    private Foo m_foo;

    private Bar m_bar;

}

我怀疑 Foo.Dispose 可能会抛出异常,因此以下代码不会被执行,所以 m_bar 没有被处理。

由于 Foo/Bar 可能来自第三方,所以不能保证不会抛出异常。

如果只是将所有的 Dispose 调用都包装在 try-catch 块中,那么代码就会变得笨拙。

如何处理这种情况是最佳实践?

7个回答

37

的确,从dispose方法泄漏出异常是很糟糕的事情,特别是实现IDisposable接口的内容通常会指定一个Finalizer来调用Dispose方法。

问题在于通过处理异常将问题掩盖起来可能会导致一些非常难以调试的情况。如果您的IDisposable分配了某种关键部分,只有在dispose后才能释放它,那么忽略异常的事实可能会导致死锁。我认为,在Dispose中出现故障应该是您想要尽早失败的情况之一,这样您就可以尽快修复错误。

当然,这都取决于被处理的对象,对于某些对象,您可能能够恢复,对于其他对象则不是。作为一般准则,Dispose在正确使用时不应抛出异常,您不应该在调用嵌套的Dispose方法时编写防御性代码。

难道您真的不想将OutOfMemoryException掩盖起来吗?

如果我有一个不稳定的第三方组件,在Dispose时任意抛出异常,我会修复它并将其托管在单独的进程中,这样当它开始出现问题时,我可以关闭它。


8
如果在终结上下文中调用Dispose()并且它抛出异常,那么您的进程将被终止。如果您怀疑Foo.Dispose()正在抛出异常,最好是在可能的情况下最后处理它,并将其包装在try/catch语句中。尽一切可能在catch中摆脱它-将引用设置为null。从Dispose()中抛出异常非常糟糕,应该避免。不幸的是,如果这是有缺陷的第三方代码,您最好让他们来修复它。您不应该手动清理它。希望能对您有所帮助。

3
@womp,这是个双输局面。如果你隐藏/处理从dispose方法抛出的任意异常,可能会导致内存、句柄或操作系统锁泄漏,这些问题都可能对进程造成严重影响。请注意不要改变原文意思。 - Sam Saffron
非常正确。我假设他想不惜一切代价继续这个过程,但我可能应该说明一下。他最好的选择肯定是放弃控制权或让第三方修复它。你关于“难以调试”的评论是一个很好的观点。 - womp

6
根据 设计规则

“IDisposable.Dispose 方法不应抛出异常。”

因此,如果因未处理的 Dispose() 异常导致程序崩溃,请参考 官方解决方案

3

因为你不需要在 using() 语句中分配变量 - 所以为什么不使用“堆叠”的 using 语句呢?


(因为使用堆叠的 using 语句可以更加简洁明了地管理资源。)
void Dispose()
{
    // the example in the question didn't use the full protected Dispose(bool) pattern
    // but most code should have if (!disposed) { if (disposing) { ...

    using (m_foo)
    using (m_bar)  
    {
        // no work, using statements will check null 
        // and call Dispose() on each object
    }

    m_bar = null;
    m_foo = null;
}

"'Stacked' using语句会被展开,如下所示:"
using (m_foo)
{
    using (m_bar) { /* do nothing but call Dispose */ }
}

因此,Dispose()调用被放置在单独的finally块中:
try {
    try { // do nothing but call Dispose
    }
    finally { 
        if (m_bar != null)
            m_bar.Dispose(); 
    }
finally { 
    if (m_foo != null)
        m_foo.Dispose();
}

我曾经很难在一个地方找到这方面的参考资料。关于“堆叠”使用语句,可以在Joe Duffy的旧博客中找到(见“C#和VB使用语句,C++堆栈语义”部分)。许多有关IDisposable的StackOverflow答案都引用了Joe Duffy的帖子。我还发现了一个最近的问题,其中对于本地变量的堆叠使用语句似乎很常见。我无法在任何地方找到finally块的链接,但是在C#语言规范(C# 3.0规范第8.13节)中,只针对单个'using'块内的多个变量进行链接,这并不完全符合我的建议,但是如果您反编译IL代码,您会发现try/finally块被嵌套。关于空值检查,也来自C#规范:“如果获取了空资源,则不调用Dispose,并且不抛出异常。”


1

7
尽管尽可能应该设计Dispose的语义以确保它不会失败,但现实世界并不总是那么完美。例如,处理文件时,Dispose应确保任何未完成写入的数据都得到写入。如果数据没有被写入,那将是一个问题,并且需要以某种方式进行通信。我建议,除非出现非常严重的问题,否则不应抛出异常,但如果真的出现了非常严重的问题,则不应忽略它。 - supercat
我理解你的观点。然而,Dispose 不应该抛出任何异常。可以在 Flush 方法或类似方法中强制写入挂起数据。我的意思是,你总是有可能编写代码以确保 Dispose 不会抛出任何异常。但是,如果你必须处理在 Dispose 中抛出异常的外部代码,我建议将其包装在其他方法中,例如 TryDispose,并以正确的方式重写 Dispose。 - LukeSw
@LukeSw:如果代码对Dispose后系统状态做出了假设,而这些假设没有得到满足,Dispose应该抛出异常。不幸的是,我们无法知道外部代码将会做出什么样的假设,也无法知道在处理失败时会发生什么异常(如果有的话)。如果某些代码期望数据库事务要么成功,要么使数据库保持在已知未更改的状态,但由于某种原因发生了异常,而数据库无法回滚并确认其是否起作用... - supercat
2
@LukeSw:...适当的行动应该是抛出一个不同于“原始”异常的异常,但它包含原始异常作为InnerException。代码可能已经准备好处理原始异常并继续处理,但这并不意味着它已经准备好处理处于未知状态的数据库。真正需要的是一个静态的PendingException方法,该方法可以用来在DatabaseRollbackException中提供InnerException。不幸的是,没有干净的方法可以获取它。 - supercat
@supercat:我同意。应该抛出一个异常,通知释放失败和应用程序未知状态(可能是危险的),但仅适用于关键异常。请参见此处:https://dev59.com/EHRB5IYBdhLWcg3wl4Oc - LukeSw
@LukeSW:我想知道微软是否会添加类似于PendingException方法的东西?这将使事情变得更加清晰。 - supercat

0
在我的情况下,是因为一个线程在关闭窗体时访问了UI元素。我通过在窗体关闭时中止线程来解决这个问题。(“FormClosing”事件)
FormClosing += (o, e) => worker.Abort();

0
为了避免重复编写释放对象的代码,我编写了以下静态方法。
    public static void DisposeObject<T>(ref T objectToDispose) where T : class
    {
        IDisposable disposable = objectToDispose as IDisposable;
        if (disposable == null) return;

        disposable.Dispose();
        objectToDispose = null;
    }

重点是你可以把它变成一个函数,这样你只需为每个要处理的对象输入一行代码,使Dispose方法变得更加简洁。在我们的案例中,惯例是将已处理指针置空,因此使用了ref参数。

在您的情况下,您可能希望添加异常处理,或者创建另一种具有异常处理的味道。我会确保在Dispose()抛出异常时记录/断点,但如果无法防止问题扩散,那么下一件最好的事情就是确保问题不会扩散。


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