在使用 'using' 块时,是否存在 Dispose 不会被调用的情况?

66

这是我在电话面试中遇到的问题:是否存在一种情况,即使用块声明作用域的对象不会调用Dispose?

我的答案是否定的-即使在using块期间出现异常,Dispose仍将被调用。

面试官不同意我的观点,并表示如果try-catch块包含using,则当您进入catch块时将不会调用Dispose。

这与我的理解相反,我没有找到任何支持面试官观点的东西。他是正确的还是我可能误解了问题?


10
面试官是错的,不要接受这份工作!最重要的是,你想为喜欢在周围布置地雷的人工作吗? - Jeremy McGee
7
如果你有面试官的电子邮件地址,回复邮件并展示代码以证明已调用dispose函数。如果他对此不感冒,那就不要为他工作 :) - AndrewC
9
接受这份工作吧!他们需要你! - VdesmedT
8
也许你接受了和Alex Papadimoulis一样的面试?http://thedailywtf.com/Articles/My-Tales.aspx - Eric Lippert
3
EMP可以做到。一般的电源故障也能造成这种情况。 - dotjoe
显示剩余8条评论
8个回答

58

18
按照概率顺序列出; - serega
1
因为#2,我们是否应该在每个使用块上标记“放射性危险”标志?请给予建议!!!1111 http://en.wikipedia.org/wiki/File:Radioactive.svg - IgorK
6
Environment.Failfast如下所示。 http://msdn.microsoft.com/en-us/library/ms131100.aspx(注:该链接为英文原文,请自行使用翻译软件进行翻译) - WernerCD
1
也许甚至ExecutionEngineException也会打破finally块,但测试起来相当复杂 :-) - xanatos
你忘了另一个问题:如果DisposableThing的构造函数抛出异常,那么using (var thing = new DisposableThing())将无法清理它。虽然using语句本身无法清理它,但不幸的是,DisposeableThing也没有特别好的模式来确保自己的清理。更糟糕的是,如果vb.net或C#中派生类的构造函数抛出异常,那么可处理的基类几乎无法确保清理[C++/CLI将自动“Dispose”部分构造的对象,但vb.net和C#几乎强制放弃]。 - supercat
显示剩余2条评论

53
void Main()
{
    try
    {
        using(var d = new MyDisposable())
        {
            throw new Exception("Hello");
        }
    }
    catch
    {
        "Exception caught.".Dump();
    }

}

class MyDisposable : IDisposable
{
    public void Dispose()
    {
        "Disposed".Dump();
    }
}

这会产生:

Disposed
Exception caught

所以我同意你的观点,而不是那个聪明的面试官...


5
为什么这个回答被认为是正确的?这不是科学工作的方式,你不能只是选一个特定的、量身定制的例子,然后宣布你的理论完美无缺......如果你想知道的话,你完全错了,Dispose可能不会被调用。 - Blindy
7
我很抱歉激怒了你。你在招人吗?;-) - VdesmedT
29
“面试官不同意,并表示如果使用try catch块包装,则在进入catch块时Dispose将不会被调用。”我认为这个答案是对面试官“理论”的很好的反例。 - anton.burger
3
@Will,Øyvind已经为我完成了那个任务,为什么要重新输入呢?而且我从来没有试图“证明” Dispose 不会被调用,VdesmedT尝试了并选择了一个可行的示例。我认为我的观点依然站得住脚。 - Blindy
2
@Blindy:呸。不要给我看你那傲慢的维基百科科学方法定义链接。说你可以证明一个事实是错误的,因为在证明过程中可能会有一颗流星砸穿天花板并干扰实验,这简直荒谬至极。而且我没看到*VdesmedT做到了,他试图选择一个可行的例子。再次问你能否给我一个例子,在这个例子中可能发生这种情况(不包括任何涉及哥斯拉的情况)? - user1228
显示剩余5条评论

25

今天早上我偶然读到一篇文章,讲述在使用using块时Dispose方法不会被调用的情况。请看这篇MSDN博客,它涉及在使用IEnumerable和yield关键字时,当你没有迭代整个集合时如何使用Dispose。

不幸的是,这并没有处理异常情况,老实说我不太确定这种情况该怎么处理。我本以为会被处理掉,但也许值得通过一小段代码来检查一下?


7
帖子中描述的行为非常合理。在一个迭代器(yield)方法中调用Dispose,只有当返回的迭代器的消费者调用IEnumerator上的Dispose方法,该方法才会被调用。换句话说:“如果不调用Dispose,就不会调用Dispose”,这显然是微不足道的。解决方法是:将对IEnumerator的使用封装在using块中,或者使用C#的foreach语句进行迭代。 - Steven

21

其他回答关于电源故障、Environment.FailFast()、迭代器或通过使用 null 的东西来作弊都很有趣。但我发现很奇怪,没有人提到我认为是最常见的情况,即使在 using 存在的情况下也不会调用 Dispose():当 using 内部的表达式抛出异常时。

当然,这是合乎逻辑的:在 using 中的表达式抛出了异常,因此赋值没有发生,我们就无法调用 Dispose()。但是可释放对象可能已经存在,尽管它可能处于半初始化状态。即使在这种状态下,它也可能已经持有一些非托管资源。这是正确实现可释放模式的另一个原因。

有问题的代码示例:

using (var f = new Foo())
{
    // something
}

…

class Foo : IDisposable
{
    UnmanagedResource m_resource;

    public Foo()
    {
        // obtain m_resource

        throw new Exception();
    }

    public void Dispose()
    {
        // release m_resource
    }
}
在这里,看起来像是Foo正确释放了m_resource并且我们也正确使用了using。但是由于异常,Foo上的Dispose()从未被调用。在这种情况下的解决方法是使用终结器,在那里释放资源。

4
非常好的发现。实际上,我会说“修复”的方法是根本不允许在构造函数中分配非托管资源。所有对这些资源的访问都应该使用延迟加载来完成。 - Sunny Milenov
1
@HemalPandya,我认为在C++中RAII更加重要,因为它没有垃圾回收器或using。但我不认为这是反对在构造函数中进行初始化的论点。如果需要,这更多地是一个正确实现终结器的论点。 - svick
但是终结器仅适用于非托管资源,对于在构造函数中初始化的托管资源该怎么办?不让异常从IDisposable构造函数中抛出是否更好? - Miserable Variable
1
在终结器中不需要释放托管资源,因为它们将被(或可能已经被)垃圾回收。如果这些资源拥有某些非托管资源,它们应该在自己的终结器中处理它们。 - svick
1
这个答案特别有趣,因为它可能意味着面试官在某种程度上是正确的。我不同意修复方法,我更喜欢如果构造函数可以抛出异常,那么资源应该在构造函数的catch块中释放并重新抛出异常,因为不能保证你的终结器会在何时被调用。 - Yaur
显示剩余2条评论

19

using 块被编译器转换为一个独立的 try/finally 块,并且位于现有的 try 块内。

例如:

try 
{
    using (MemoryStream ms = new MemoryStream())
        throw new Exception();
}
catch (Exception)
{
    throw;
}
成为。
.try
{
  IL_0000:  newobj     instance void [mscorlib]System.IO.MemoryStream::.ctor()
  IL_0005:  stloc.0
  .try
  {
    IL_0006:  newobj     instance void [mscorlib]System.Exception::.ctor()
    IL_000b:  throw
  }  // end .try
  finally
  {
    IL_000c:  ldloc.0
    IL_000d:  brfalse.s  IL_0015
    IL_000f:  ldloc.0
    IL_0010:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0015:  endfinally
  }  // end handler
}  // end .try
catch [mscorlib]System.Exception 
{
  IL_0016:  pop
  IL_0017:  rethrow
}  // end handler
编译器不会重新排列语句顺序,因此执行顺序如下:
1. 在使用块的 try 部分中抛出或传播异常 2. 控制流离开使用块的 try 部分,并进入其 finally 部分 3. 最终块中的代码将对象处理完毕 4. 控制流离开 finally 块,并将异常传播到外部的 try 中 5. 控制流离开外部的 try 并进入异常处理程序
重点是,内部的 finally 块总是在外部的 catch 块之前运行,因为直到 finally 块结束之前,异常才会传播。唯一正常情况下无法保证这种情况发生的情况是在生成器(迭代器)中。迭代器被转换为一个半复杂的状态机,如果在 yield return 之后(但在被处理前)变得不可达,则不能保证 finally 块会运行。

1
finally 块会在控制权离开 try 块后发生。 - Eric Lippert
@Eric:我实际上是指“之前”,因为我在谈论的是外部 try 而不是 using 生成的 try。但是当你同时谈论两者时,会有点混淆。希望我澄清了一些。 - cHao

13
using (var d = new SomeDisposable()) {
    Environment.FailFast("no dispose");
}

3
如果您能提供更多信息,那将非常好:http://msdn.microsoft.com/en-us/library/ms131100.aspx - WernerCD

5

是的,有一种情况下dispose方法不会被调用...你想得太多了。这种情况是使用块中的变量为null时。

class foo
{
    public static IDisposable factory()
    {
        return null;
    }
}

using (var disp = foo.factory())
{
    //do some stuff
}

如果在任何情况下都调用了dispose,这个代码不会引发异常。但是你的面试官提到的特定情况是错误的。


0
面试官的部分观点是正确的。在特定情况下,Dispose可能无法正确清理底层对象。
例如,WCF在使用块中抛出异常时存在一些已知问题。你的面试官可能正在考虑这个问题。
这里有一篇来自MSDN的文章,介绍如何避免在WCF中使用块时出现问题。这里是微软官方的解决方法,尽管我现在认为结合这个答案是最优雅的方法。

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