永远不应使用using语句和IDisposable的情况

8
我正在阅读这种情况,在使用C# using语句时可能会出现问题。如果在using块的范围内抛出异常,并且在using语句结束时调用的Dispose函数也抛出异常,则可能会丢失异常。这突显了在某些情况下决定是否添加using语句时应当小心。
我只在使用流和派生自DbConnection的类时才会使用using语句。如果需要清理非托管资源,我通常会使用finally块。
这是IDisposable接口的另一种用法,用于创建性能计时器,该计时器将在Dispose函数中停止计时并将时间记录到注册表中。http://thebuildingcoder.typepad.com/blog/2010/03/performance-profiling.html 这是IDisposable接口的好用法吗?它没有清理资源或处理任何其他对象。但是,我可以看出它如何通过将要分析的代码整洁地包装在using语句中来清理调用代码。

在使用using语句和IDisposable接口时,是否有不应该使用的情况?实现IDisposable或将代码包装在using语句中是否曾经给您带来问题?

谢谢

5个回答

5
我认为,除非文档告诉你不要使用(就像你的例子一样),否则始终使用using
Dispose方法抛出异常有点违背使用它的初衷(双关语)。每当我实现它时,我总是尽力确保无论对象处于什么状态,都不会抛出任何异常。
附注:这里有一个简单的实用程序方法来弥补WCF的行为。这确保在除调用Close之外的所有执行路径中调用Abort,并将错误传播给调用者。
public static void CallSafely<T>(ChannelFactory<T> factory, Action<T> action) where T : class {
    var client = (IClientChannel) factory.CreateChannel();
    bool success = false;
    try {
        action((T) client);
        client.Close();
        success = true;
    } finally {
        if(!success) {
            client.Abort();
        }
    }
}

如果您在框架的其他地方发现任何有趣的行为案例,可以提出类似的策略来处理它们。


1
没错。在我看来,任何抛出异常的 Dispose 方法都是有问题的。这就像从 C++ 析构函数中抛出异常一样(是不允许的)。 - Stephen Cleary
2
@Abel:非常糟糕的想法。你打算在每个发布版本中使用Reflector来确保它们不会在Dispose中实际执行某些操作吗? - John Saunders
1
@Abel:很多IEnumerator类没有实现IDisposable,但如果它们实现了,我肯定会调用Dispose或者直接使用foreach结构,因为它会自动调用Dispose。关于控件...在主流场景中,你不需要这样做,因为它们通常包含在另一个作为容器的FormControl中,当必要时会正确地调用Dispose - Brian Gideon
@Brian Gideon: 好观点(但 IEnumerator <>派生类始终实现IDisposable)。我认为我有点被误解了,因为我个人赞成使用using。但是有时候这是不可行的(长时间存活的对象,即在会话或缓存中),或者根本不需要。与 Hans Passant进行这次讨论的一个有趣的副作用是他赞成使用using:https://dev59.com/2nA75IYBdhLWcg3wf5RV#3257928 - Abel
@Abel:对于长寿命对象(或其作用域不仅限于单个方法的对象),显然 using 没有多大用处,但请记得手动调用 Dispose。顺便说一下,我没有看到 Hans 倾向于不使用 using,但是我对那个链接中的交流感到相当困惑。 - Brian Gideon
显示剩余2条评论

4
一般的经验法则很简单:当一个类实现了IDisposable接口时,使用using。当你需要捕获错误时,使用try/catch/finally,以便能够捕获错误。
然而,有几点需要注意。
  1. You ask whether situations exist where IDisposable should not be used. Well: in most situations you shouldn't need to implement it. Use it when you want to free up resources timely, as opposed to waiting until the finalizer kicks in.

  2. When IDisposable is implemented, it should mean that the corresponding Dispose method clears its own resources and loops through any referenced or owned objects and calls Dispose on them. It should also flag whether Dispose is called already, to prevent multiple cleanups or referenced objects to do the same, resulting in an endless loop. However, all this is no guarantee that all references to the current object are gone, which means it will remain in memory until all references are gone and the finalizer kicks in.

  3. Throwing exceptions in Dispose is frowned upon and when it happens, state is possibly not guaranteed anymore. A nasty situation to be in. You can fix it by using try/catch/finally and in the finally block, add another try/catch. But like I said: this gets ugly pretty quickly.

  4. Using using is one thing, but don't confuse it with using try/finally. Both are equal, but the using-statement makes life easier by adding scoping and null-checks, which is a pain to do by hand each time. The using-statement translates to this (from C# standard):

    {
        SomeType withDispose = new SomeType();
        try
        {
             // use withDispose
        }            
        finally 
        {
            if (withDispose != null)
            {
                 ((IDisposable)withDispose).Dispose();
            }
        }
    }
    
  5. There are occasions where wrapping an object into a using-block is not necessary. These occasions are rare. They happen when you find yourself inheriting from an interface that inherits from IDisposable just in case a child would require disposing. An often-used example is IComponent, which is used with every Control (Form, EditBox, UserControl, you name it). And I rarely see people wrapping all these controls in using-statements. Another famous example is IEnumerator<T>. When using its descendants, one rarely sees using-blocks either.

结论

普遍使用 using 语句,并谨慎考虑其他选择或不使用它。确保您知道使用(或不使用)的影响,并了解 using 和 try/finally 的相等性。需要捕获任何异常吗?请使用 try/catch/finally。


2
我认为更大的问题是在Dispose中抛出异常。 RAII模式通常明确声明不应该这样做,因为它可能会创建像这样的情况。我的意思是,如果某些东西没有正确释放,除了简单地结束执行之外,还有什么恢复路径呢?
此外,似乎可以通过两个try-catch语句来避免这种情况:
try
{
    using(...)
    {
        try
        {
            // Do stuff
        }
        catch(NonDisposeException e)
        {
        }
    }
}
catch(DisposeException e)
{
}

这里唯一可能出现的问题是,如果DisposeExceptionNonDisposeException相同或是其超类型,并且您试图在NonDisposeException捕获块中重新抛出异常。在这种情况下,将会被DisposeException块捕获。因此,您可能需要一些额外的布尔标记来检查此情况。


我同意你的观察,即在Dispose内部抛出异常是不可取的。然而,如果您完全删除using块并添加finally语句,则您的代码可能会更易读一些。但这当然是一个风格和观点问题。无论您尝试什么,它都很丑陋(这就是为什么Dispose不能抛出异常,也不能有终结器的原因!) - Abel

2
我所知道的唯一情况是WCF客户端。这是由于WCF中的设计缺陷 - Dispose不应该抛出异常。他们错过了这个问题。

1

一个例子是 IAsyncResult.AsyncWaitHandle 属性。敏锐的程序员会意识到 WaitHandle 类实现了 IDisposable 接口,自然而然地试图贪婪地释放它们。但是,在 BCL 中大多数 APM 的实现实际上在属性内部进行惰性初始化 WaitHandle。显然,结果是程序员做了比必要更多的工作。

那么问题出在哪里呢?嗯,微软搞砸了 IAsyncResult 接口。如果他们遵循自己的建议,IAsyncResult 将从 IDisposable 派生,因为暗示它持有可释放资源。敏锐的程序员随后只需调用 IAsyncResult 上的 Dispose 方法,并让其决定如何最好地处理其组成部分的释放。

这是经典边缘案例之一,其中处理 IDisposable 可能会有问题。Jeffrey Richter 实际上使用这个例子来争论(在我看来是不正确的),即调用 Dispose 不是强制性的。你可以在 这里 阅读这场辩论。


拥有IDisposable对象的对象应该处理它们。创建IDisposable对象Foo的对象应该是Foo的初始所有者,但预期它成为Foo.SomeProperty的所有者非常奇怪,除非它采取一些明确获得此类所有权的操作(例如调用CreateWaitHandle方法,然后指示调用方是否应该承担所讨论的WaitHandle的所有权)。 我可以看出,如果非空,则尝试将IAsyncResult转换为IDisposable并调用Dispose是有基础的(对于非泛型IEnumerator这样的模式是适当的... - supercat
否则,我认为除了IAsyncResult以外,没有任何依据表明它拥有其WaitHandle的所有权。 - supercat
@supercat:我同意。它应该“拥有”那个WaitHandle。问题在于,因为IAsyncResult没有实现IDisposable,它迫使程序员在只有两个糟糕的选择之间进行选择:1)保留底层的WaitHandle未释放或2)假设拥有权并试图贪婪地释放它。#1是不好的,因为它会让系统资源处于活动状态的时间比必要的时间更长。#2是不好的,因为它偏离了标准实践,并且可能会强制分配资源,当它本来不需要时,因为它是惰性初始化的。因此,Jeffrey Richter的论点是错误的。 - Brian Gideon
你忽略了我上面提到的第三种选择:测试特定实现IAsyncResult接口的对象是否实现了IDisposable,如果是,则调用Dispose方法,并认为任何未实现IDisposable接口的IAsyncResult实现都必须返回只有终结器可以提供足够清理的类型的AsyncWaitHandle属性。如果Richter先生的论点是存在一些设计不良的实现IDisposable接口的类,其实例应该在不调用Dispose的情况下被丢弃,我完全同意他的观点;StreamReader就是一个典型的例子。 - supercat
如果有人想争论没有任何设计良好的类实现IDisposable,其实例不应被放弃处理,我可能更倾向于同意,尽管不是100%。一些类型的对象实现了IEnumerator<T>,但并不使用任何资源。如果代码显式创建此类类型的实例(而不是通过GetEnumerator()调用接收一个),并且跟踪这些实例的生命周期很困难,则放弃该实例可能比为确保处理而复杂化代码更好。 - supercat

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