可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
This was a telephone interview question I had: Is there a time when Dispose will not be called on an object who's scope is declared by a using block?
My answer was no - even if an exception happens during the using block, Dispose will still be called.
The interviewer disagreed and said if using
is wrapped in a try
-catch
block then Dispose will not be called by the time you enter the catch block.
This goes contrary to my understanding of the construct, and I haven't been able to find anything that backs up the interviewers point of view. Is he correct or might I have misunderstood the question?
回答1:
void Main()
{
try
{
using(var d = new MyDisposable())
{
throw new Exception("Hello");
}
}
catch
{
"Exception caught.".Dump();
}
}
class MyDisposable : IDisposable
{
public void Dispose()
{
"Disposed".Dump();
}
}
This produced :
Disposed
Exception caught
So I agree with you and not with the smarty interviewer...
回答2:
Four things that will cause Dispose to not be called in a using block:
- A power failure on your machine when inside the using block.
- Your machine getting melted by an atomic bomb while in the inside of the using block.
- Uncatchable exceptions like
StackOverflowException
, AccessViolationException
and possibly others.
- Environment.FailFast
回答3:
Bizarrely I read about a circumstance where Dispose won't get called in a using block just this morning. Checkout this blog on MSDN. It's around using Dispose with IEnumerable and the yield keyword, when you don't iterate the entire collection.
Unfortunately this doesn't deal with the exception case, honestly I'm not sure about that one. I would have expected it to be done but maybe it's worth checking with a quick bit of code?
回答4:
The other answers about power failure, Environment.FailFast()
, iterators or cheating by using
something that is null
are all interesting. But I find it curious that nobody mentioned what I think is the most common situation when Dispose()
won't be called even in the presence of using
: when the expression inside using
throws an exception.
Of course, this is logical: the expression in using
threw an exception, so the assignment didn't take place and there is nothing we could call Dispose()
on. But the disposable object can already exist, although it can be in half initialized state. And even in this state it can already hold some unmanaged resources. This is another reason why correctly implementing the disposable pattern is important.
Example of the problematic code:
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
}
}
Here, it looks like Foo
releases m_resource
correctly and we are using using
correctly too. But the Dispose()
on Foo
is never called, because of the exception. The fix in this case is to use finalizer and release the resource there too.
回答5:
The using
block gets turned by the compiler into a try
/finally
block of its own, within the existing try
block.
For example:
try
{
using (MemoryStream ms = new MemoryStream())
throw new Exception();
}
catch (Exception)
{
throw;
}
becomes
.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
The compiler won't rearrange things. So it happens like this:
- Exception is thrown in, or propagates to, the
using
block's try
part
- Control leaves the
using
block's try
part, and enters its finally
part
- Object is disposed by the code in the
finally
block
- Control leaves the finally block, and the exception propagates out to the outer
try
- Control leaves the outer
try
and goes into the exception handler
Point being, the inner finally
block always runs before the outer catch
, because the exception doesn't propagate til the finally
block finishes.
The only normal case where this won't happen, is in a generator (excuse me, "iterator").
An iterator gets turned into a semi-complicated state machine, and finally
blocks are not guaranteed to run if it becomes unreachable after a yield return
(but before it has been disposed).
回答6:
using (var d = new SomeDisposable()) {
Environment.FailFast("no dispose");
}
回答7:
Yes there is a case when dispose won't be called... you are over thinking it. The case is when the variable in the using block is null
class foo
{
public static IDisposable factory()
{
return null;
}
}
using (var disp = foo.factory())
{
//do some stuff
}
will not throw an exception but would if dispose was called in every case. The specific case that your interviewer mentioned is wrong though.
回答8:
The interviewer is partially right. Dispose
may not correctly clean up the underlying object on a case-by-case basis.
WCF for example has a few known issues if an exception is thrown while in a using block. Your interviewer was probably thinking of this.
Here is an article from MSDN on how to avoid issues with the using block with WCF. Here is Microsoft's official workaround, although I now think that a combination of that answer and this one is the most elegant approach.