可以“使用”多个资源导致资源泄漏?
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); } }