“使用”多个资源会导致资源泄漏吗?

C # 允许我执行以下操作(来自 MSDN 的示例) :

using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}

如果 font4 = new Font抛出会发生什么? 据我所知,font3将泄露资源并且不会被处理掉。

  • 这是真的吗? (字体4将不会被处理)
  • 这是否意味着应该完全避免使用 using(... , ...)来支持嵌套使用?
7757 次浏览

No.

The compiler will generate a separate finally block for each variable.

The spec (§8.13) says:

When a resource-acquisition takes the form of a local-variable-declaration, it is possible to acquire multiple resources of a given type. A using statement of the form

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement

is precisely equivalent to a sequence of nested using statements:

using (ResourceType r1 = e1)
using (ResourceType r2 = e2)
...
using (ResourceType rN = eN)
statement

As a complement to @SLaks answer, here's the IL for your code:

.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 74 (0x4a)
.maxstack 2
.entrypoint
.locals init (
[0] class [System.Drawing]System.Drawing.Font font3,
[1] class [System.Drawing]System.Drawing.Font font4,
[2] bool CS$4$0000
)


IL_0000: nop
IL_0001: ldstr "Arial"
IL_0006: ldc.r4 10
IL_000b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
IL_0010: stloc.0
.try
{
IL_0011: ldstr "Arial"
IL_0016: ldc.r4 10
IL_001b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
IL_0020: stloc.1
.try
{
IL_0021: nop
IL_0022: nop
IL_0023: leave.s IL_0035
} // end .try
finally
{
IL_0025: ldloc.1
IL_0026: ldnull
IL_0027: ceq
IL_0029: stloc.2
IL_002a: ldloc.2
IL_002b: brtrue.s IL_0034


IL_002d: ldloc.1
IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0033: nop


IL_0034: endfinally
} // end handler


IL_0035: nop
IL_0036: leave.s IL_0048
} // end .try
finally
{
IL_0038: ldloc.0
IL_0039: ldnull
IL_003a: ceq
IL_003c: stloc.2
IL_003d: ldloc.2
IL_003e: brtrue.s IL_0047


IL_0040: ldloc.0
IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0046: nop


IL_0047: endfinally
} // end handler


IL_0048: nop
IL_0049: ret
} // end of method Program::Main

Note the nested try/finally blocks.

Here is a sample code to prove @SLaks answer:

void Main()
{
try
{
using (TestUsing t1 = new TestUsing("t1"), t2 = new TestUsing("t2"))
{
}
}
catch(Exception ex)
{
Console.WriteLine("catch");
}
finally
{
Console.WriteLine("done");
}


/* outputs


Construct: t1
Construct: t2
Dispose: t1
catch
done


*/
}


public class TestUsing : IDisposable
{
public string Name {get; set;}


public TestUsing(string name)
{
Name = name;


Console.WriteLine("Construct: " + Name);


if (Name == "t2") throw new Exception();
}


public void Dispose()
{
Console.WriteLine("Dispose: " + Name);
}
}

This code (based on the original sample):

using System.Drawing;


public class Class1
{
public Class1()
{
using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
}
}

It produces the following CIL (in Visual Studio 2013, targeting .NET 4.5.1):

.method public hidebysig specialname rtspecialname
instance void  .ctor() cil managed
{
// Code size       82 (0x52)
.maxstack  2
.locals init ([0] class [System.Drawing]System.Drawing.Font font3,
[1] class [System.Drawing]System.Drawing.Font font4,
[2] bool CS$4$0000)
IL_0000:  ldarg.0
IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
IL_0006:  nop
IL_0007:  nop
IL_0008:  ldstr      "Arial"
IL_000d:  ldc.r4     10.
IL_0012:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
float32)
IL_0017:  stloc.0
.try
{
IL_0018:  ldstr      "Arial"
IL_001d:  ldc.r4     10.
IL_0022:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
float32)
IL_0027:  stloc.1
.try
{
IL_0028:  nop
IL_0029:  nop
IL_002a:  leave.s    IL_003c
}  // end .try
finally
{
IL_002c:  ldloc.1
IL_002d:  ldnull
IL_002e:  ceq
IL_0030:  stloc.2
IL_0031:  ldloc.2
IL_0032:  brtrue.s   IL_003b
IL_0034:  ldloc.1
IL_0035:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
IL_003a:  nop
IL_003b:  endfinally
}  // end handler
IL_003c:  nop
IL_003d:  leave.s    IL_004f
}  // end .try
finally
{
IL_003f:  ldloc.0
IL_0040:  ldnull
IL_0041:  ceq
IL_0043:  stloc.2
IL_0044:  ldloc.2
IL_0045:  brtrue.s   IL_004e
IL_0047:  ldloc.0
IL_0048:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
IL_004d:  nop
IL_004e:  endfinally
}  // end handler
IL_004f:  nop
IL_0050:  nop
IL_0051:  ret
} // end of method Class1::.ctor

As you can see, the try {} block doesn't start until after the first allocation, which takes place at IL_0012. At first glance, this does appear to allocate the first item in unprotected code. However, notice that the result is stored in location 0. If the second allocation then fails, the outer finally {} block executes, and this fetches the object from location 0, i.e. the first allocation of font3, and calls its Dispose() method.

Interestingly, decompiling this assembly with dotPeek produces the following reconstituted source:

using System.Drawing;


public class Class1
{
public Class1()
{
using (new Font("Arial", 10f))
{
using (new Font("Arial", 10f))
;
}
}
}

The decompiled code confirms that everything is correct and that the using is essentially expanded into nested usings. The CIL code is a bit confusing to look at, and I had to stare at it for a good few minutes before I properly understood what was happening, so I'm not surprised that some 'old wives tales' have started to sprout up about this. However, the generated code is the unassailable truth.

UPDATE: I used this question as the basis for an article which can be found here; see it for additional discussion of this issue. Thanks for the good question!


Though Schabse's answer is of course correct and answers the question that was asked, there is an important variant on your question you did not ask:

What happens if font4 = new Font() throws after the unmanaged resource was allocated by the constructor but before the ctor returns and fills in font4 with the reference?

Let me make that a little bit more clear. Suppose we have:

public sealed class Foo : IDisposable
{
private int handle = 0;
private bool disposed = false;
public Foo()
{
Blah1();
int x = AllocateResource();
Blah2();
this.handle = x;
Blah3();
}
~Foo()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposed)
{
if (this.handle != 0)
DeallocateResource(this.handle);
this.handle = 0;
this.disposed = true;
}
}
}

Now we have

using(Foo foo = new Foo())
Whatever(foo);

This is the same as

{
Foo foo = new Foo();
try
{
Whatever(foo);
}
finally
{
IDisposable d = foo as IDisposable;
if (d != null)
d.Dispose();
}
}

OK. Suppose Whatever throws. Then the finally block runs and the resource is deallocated. No problem.

Suppose Blah1() throws. Then the throw happens before the resource is allocated. The object has been allocated but the ctor never returns, so foo is never filled in. We never entered the try so we never enter the finally either. The object reference has been orphaned. Eventually the GC will discover that and put it on the finalizer queue. handle is still zero, so the finalizer does nothing. Notice that the finalizer is required to be robust in the face of an object that is being finalized whose constructor never completed. You are required to write finalizers that are this strong. This is yet another reason why you should leave writing finalizers to experts and not try to do it yourself.

Suppose Blah3() throws. The throw happens after the resource is allocated. But again, foo is never filled in, we never enter the finally, and the object is cleaned up by the finalizer thread. This time the handle is non-zero, and the finalizer cleans it up. Again, the finalizer is running on an object whose constructor never succeeded, but the finalizer runs anyways. Obviously it must because this time, it had work to do.

Now suppose Blah2() throws. The throw happens after the resource is allocated but before handle is filled in! Again, the finalizer will run but now handle is still zero and we leak the handle!

You need to write extremely clever code in order to prevent this leak from happening. Now, in the case of your Font resource, who the heck cares? We leak a font handle, big deal. But if you absolutely positively require that every unmanaged resource be cleaned up no matter what the timing of exceptions is then you have a very difficult problem on your hands.

The CLR has to solve this problem with locks. Since C# 4, locks that use the lock statement have been implemented like this:

bool lockEntered = false;
object lockObject = whatever;
try
{
Monitor.Enter(lockObject, ref lockEntered);
lock body here
}
finally
{
if (lockEntered) Monitor.Exit(lockObject);
}

Enter has been very carefully written so that no matter what exceptions are thrown, lockEntered is set to true if and only if the lock was actually taken. If you have similar requirements then what you need to to is actually write:

    public Foo()
{
Blah1();
AllocateResource(ref handle);
Blah2();
Blah3();
}

and write AllocateResource cleverly like Monitor.Enter so that no matter what happens inside AllocateResource, the handle is filled in if and only if it needs to be deallocated.

Describing the techniques for doing so is beyond the scope of this answer. Consult an expert if you have this requirement.