2014-01-14 100 views
106

C#讓我(從MSDN示例)執行以下操作:可以「使用」多個資源導致資源泄漏?

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

如果font4 = new Font拋出,會發生什麼?從我所瞭解的字體3將泄漏資源,不會被處置。

  • 這是真的嗎? (font4將不會被處置)
  • 這是否意味着using(... , ...)應該完全避免嵌套使用?
+7

它不會_leak_記憶;在最壞的情況下,它仍然會GC'd。 – SLaks

+0

@SLaks,所以底層資源總是會被處置? 'font3.Dispose()'肯定會被調用? –

+3

我不會感到驚訝,如果'使用(...,...)'編譯成嵌套使用塊,無論如何,但我不知道這一點。 –

回答

158

號編譯器將產生用於每個變量的單獨finally塊。

spec(§8.13)表示:

當資源採集需要 局部變量聲明的形式,所以可以獲取多個給定類型的 資源。形式

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

using聲明恰恰 相當於使用語句嵌套的序列:

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

這是8.13 C#規範版本5.0,順便說一句。 –

+1

但如果某人在using方法仍在執行時打開文件會怎樣? –

+11

@WeylandYutani:你在問什麼? – SLaks

32

爲補充@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塊。

7

這裏是一個示例代碼來證明@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); 
    } 
} 
+1

這並不能證明它。 Dispose在哪裏:t2? :) –

+1

問題是關於使用列表中的第一個資源而不是第二個。 _「如果'font4 = new Font'會出現什麼情況?從我所瞭解的字體3將會泄漏資源並且不會被丟棄。」_ – wdosanjos

17

此代碼(基於原始樣品):

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 

因爲你可以s ee,try {}塊直到第一次分配後纔會啓動,發生在IL_0012。乍一看,這是出現分配第一項在不受保護的代碼。但是,請注意結果存儲在位置0中。如果第二個分配失敗,則將執行該塊,並從位置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代碼看起來有點令人困惑,在我正確理解發生的事情之前,我只好盯着它幾分鐘,所以我並不覺得有些「老妻子的故事」已經開始萌芽這個。但是,生成的代碼是無法實現的事實。

+0

@Peter Mortensen您的編輯刪除了IL代碼(介於IL_0012和IL_0017之間)的渲染這個解釋既無效又混亂。該代碼旨在成爲我獲得的結果的一個_verbatim_副本,並編輯了無效的結果。你能否請審查你的編輯並確認這是你的意圖? –

67

UPDATE:我用這個問題作爲文章的基礎,可以找到here;請參閱此處以獲取有關此問題的其他討論感謝您的好問題!


雖然Schabse's answer當然是正確的,回答說,有人問這個問題,對你的問題的一個重要的變體,你沒有問:會發生什麼,如果font4 = new Font()拋出非託管

資源是由建設者分配的,但之前 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拋出。然後finally塊運行並且資源被解除分配。沒問題。

假設Blah1()拋出。然後在分配資源之前拋出。該對象已分配但ctor永不返回,因此foo永遠不會被填入。我們從未輸入try,所以我們從未輸入finally。對象引用已被孤立。最終GC會發現並將其放到終結器隊列中。 handle仍然是零,所以終結器什麼都不做。 請注意,終結者需要在面對正在定稿的構造函數從未完成的對象時保持健壯。你是要求來寫這個強大的終結者。這是你爲什麼應該寫完最終版給專家而又不想自己去做的另一個原因。

假設Blah3()拋出。投擲發生在資源分配後。但是再次,foo永遠不會填充,我們永遠不會輸入finally,並且該對象由終結器線程清理。這一次句柄非零,並且終結器清理它。同樣,終結器運行在構造函數從未成功的對象上,但終結器仍然運行。顯然這一定是因爲這一次,它有工作要做。

現在假設Blah2()拋出。投擲資源分配後發生,但之前handle被填入!再次,終結者將運行,但現在handle仍然是零,我們泄漏處理!

你需要編寫極其巧妙的代碼,以防止這種泄漏發生。現在,在你的Font資源的情況下,誰在乎?我們泄漏了一個字體句柄,很重要。但如果你絕對肯定要求非託管資源被清理無論什麼時候的例外是那麼你有一個非常困難的問題,在你的手中。

CLR必須用鎖來解決這個問題。由於C#4,使用該lock聲明鎖已經實施這樣的:

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

Enter已經非常仔細地編寫,以便不管是什麼引發異常lockEntered設置爲true 當且僅當鎖實際上被採取。如果您有類似的要求,那麼你需要到實際上寫的是什麼:

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

,寫AllocateResource巧妙地像Monitor.Enter因此,無論內AllocateResource發生什麼情況,handle填充當且僅當需要被解除分配。

描述這樣做的技術超出了這個答案的範圍。如果你有這個要求,請諮詢專家。

+1

你指的是什麼_Schabse的答案? – gnat

+6

@gnat:接受的答案。 S必須代表某些東西。 :-) –

+2

*我們泄漏了一個字體句柄,大問題* ...我發現如果你泄漏了一個字體句柄,你的代碼就會經常泄漏字體句柄......直到它崩潰的程度非常嚴重。看到它發生了:( – gbjbaanb