可以“使用”多个资源导致资源泄漏?

C#让我做以下(MSDN示例):

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

如果font4 = new Font引发会发生什么? 从我的理解font3将泄漏资源,将不会被处置。

  • 这是真的? (font4不会被丢弃)
  • 这是否意味着using(... , ...)应该完全避免嵌套使用?

没有。

编译器将为每个variables生成一个单独的finally块。

规范 (§8.13)说:

当资源获取采取局部variables声明的forms时,可以获取给定types的多个资源。 表格的using说明

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

恰恰相当于一系列使用嵌套的语句:

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

更新 :我用这个问题作为可以在这里find的文章的基础; 看到这个问题的额外讨论。 感谢您的好问题!


尽pipeSchabse的答案当然是正确的,并且回答了被问到的问题,但是在你没有问的问题上有一个重要的变体:

如果font4 = new Font() 非托pipe资源被构造函数分配之后 ctor返回并使用引用填充font4 之前抛出会发生什么?

让我更清楚一点。 假设我们有:

 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; } } } 

现在我们有了

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

这是一样的

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

好。 假设Whatever抛出Whatever 。 然后finally块运行,资源被释放。 没问题。

假设Blah1()抛出。 然后抛出在资源分配之前发生。 这个对象已经被分配了,但是ctor永远不会返回,所以foo永远不会被填充。我们从来没有进入try所以我们永远也不会进入finally 。 对象引用已经被孤立。 GC最终会发现并将其放在终结器队列中。 handle仍然是零,所以终结器什么都不做。 请注意,终结器必须在正在定稿的构造函数从未完成的对象面前保持健壮 。 你需要编写这个强大的终结器。 这也是你为什么要给专家写封头文,而不是自己去做的另一个原因。

假设Blah3()抛出。 投掷发生在资源分配后。 但是, foo永远不会被填充,我们永远不会进入finally ,并且通过终结器线程清理对象。 这次手柄是非零的,并且终结器清理它。 同样,终结器运行在构造函数从未成功的对象上,但是终结器仍然运行。 显然这是因为这一次,它有工作要做。

现在假设Blah2()抛出。 抛出资源分配后,但handle填充之前发生! 再次,终结者将运行,但现在handle仍然是零,我们泄漏处理!

你需要写出非常聪明的代码,以防止这种泄漏发生。 现在,在你的Font资源的情况下,谁在乎? 我们漏了一个字体句柄,大不了。 但是,如果你绝对肯定地要求清理每一个非托pipe资源, 不pipeexception的时间是什么,那么你就有一个非常困难的问题。

CLR必须用锁来解决这个问题。 从C#4开始,使用lock语句的lock就像这样实现了:

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

Enter已经非常仔细的编写了,所以无论抛出什么exception当且仅当锁被实际使用时, lockEntered被设置为true。 如果你有类似的要求,那么你需要实际写的是:

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

并像Monitor.Enter一样聪明地写AllocateResource这样无论在AllocateResource里面发生了什么, 当且仅当它需要被释放的时候, handle 才被填充。

描述这样做的技术超出了这个答案的范围。 如果您有这个要求,请咨询专家。

作为对@SLaks答案的补充,下面是代码中的IL:

 .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 

注意嵌套的try / finally块。

此代码(基于原始示例):

 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. } } } 

它生成以下CIL (在Visual Studio 2013中 ,面向.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 

正如你所看到的, try {}块在第一次分配之后才会启动,这个分配发生在IL_0012 。 乍一看,这看起来似乎分配在不受保护的代码的第一项。 但是,请注意结果存储在位置0中。如果第二个分配失败,则执行外部 finally {}块,并从位置0(即font3的第一个分配)获取对象,并调用其Dispose()方法。

有趣的是,用dotPeek反编译这个程序集会产生下面的重组源代码:

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

反编译的代码确认一切正确,并且using本质上被扩展为嵌套using 。 CIL代码看起来有点混乱,在我正确理解发生的事情之前,我只好盯着它几分钟,所以我不觉得有些“老妻子故事”已经开始萌芽了这个。 但是,生成的代码是不可撼动的事实。

这里是一个示例代码来certificate@SLaks的答案:

 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); } }