如何為我們自己的包含非托管資源的類型編寫資源管理代碼呢?在 .NET 中為我們提供了一種標准的銷毀非托管資源的模式,這個標准的模式能夠使使用者通過調用IDisposable接口正常釋放掉非托管資源,也能夠保證使用者在忘記釋放資源時使用終結器釋放。這個標准模式可以和GC配合,保證僅在最糟糕的情況下才調用終結器,盡可能的降低其帶來的性能影響。
閱讀目錄:
1.實現IDisposable接口
實現IDisposable接口是一種標准的做法,用來通知使用者和運行時系統該對象包含的資源需要及時釋放。IDisposable.Dispose()方法僅僅定義了一個方法:
1 public interface IDisposable 2 { 3 void Dispose(); 4 }
實現IDisposable.Dispose()方法需要完成以下目標:
- 釋放掉所有非托管資源
- 釋放掉所有托管資源,包括釋放事件監聽程序
- 設定一個狀態標志,表示該對象已經被銷毀
- 跳過終結操作,調用GC.SuppressFinalize(this)即可。
實現IDisposable接口應該完成兩件事:
- 提供一種機制,讓使用者可以在垃圾收集的時候及時釋放掉所有的托管資源
- 提供一種標准做法,讓使用者可以釋放掉所有的非托管資源(避免終結過程帶來的開銷)
1.1 資源釋放的標准模式
不過這里存在着問題:如何讓派生類清理自己的資源,同樣也能讓基類進行清理呢?如果派生類覆寫了終結器,或是實現了IDisposable接口,那么這些方法必須調用基類。否則,基類將不能夠被正確清理。在這里我們有一種標准的做法就是:編寫一個受保護的虛輔助方法,將銷毀和析構共同的工作提取出來,並讓派生類也可以釋放其自己的資源。基類包含了核心接口的代碼,而虛方法則為派生類提供了根據Dispose()或終結器的需要進行資源清理的入口:
1 //Dispose 虛方法 2 //將銷毀和析構共同的工作提取出來,並讓派生類也可以釋放其自己的資源 3 protected virtual void Dispose(bool isDisposing)
該重載方法需要同時支持終結器和Dispose方法,同時因為它是個虛方法所以所有得派生類都可以講其作為釋放資源的入口點。派生類可覆寫該方法,並在其中清理自身的資源,然后調用基類的版本。我們來看這一個標准模式的示例代碼:
1 public class MyResourceHog : IDisposable 2 { 3 //標記為已銷毀 4 private bool alreadyDisposed = false; 5 6 //實現IDisposable 7 //調用定義的Dispose()虛方法 8 //跳過終結器 9 public void Dispose() 10 { 11 Dispose(true); 12 GC.SuppressFinalize(this); 13 } 14 15 //Dispose 虛方法 16 //將銷毀和析構共同的工作提取出來,並讓派生類也可以釋放其自己的資源 17 //isDisposing == true 時,同時清理托管資源; 18 protected virtual void Dispose(bool isDisposing) 19 { 20 //不需要處理多次 21 if (alreadyDisposed) 22 return; 23 if (isDisposing) 24 { 25 //省略:在這里釋放托管資源 26 } 27 //省略:在這里釋放非托管資源 28 //設置已處理標志 29 alreadyDisposed = true; 30 } 31 32 public void ExampleMethod() 33 { 34 if (alreadyDisposed) 35 throw new ObjectDisposedException("MyResourceHog", "調用了已經被釋放的對象"); 36 //省略 37 } 38 }
派生類在執行自己分配的資源清理工作時,可以覆寫基類中受保護的Dispose(bool)方法,且無論isDisposing取值如何,都要調用基類的Dispose(isDisposing)方法,以便讓基類完成自身資源的釋放:
1 public class DerivedResourceHog : MyResourceHog 2 { 3 private bool disposed = false; 4 5 protected override void Dispose(bool isDisposing) 6 { 7 if (disposed) 8 return; 9 if (isDisposing) 10 { 11 //這里釋放托管資源 12 } 13 //釋放非托管資源 14 15 //這里釋放基類資源 16 //基類負責調用 17 // GC.SuppressFinalize(this); 18 base.Dispose(isDisposing); 19 20 //設置已經被銷毀的標志 21 disposed = true; 22 } 23 }
我們可以觀察到前面的示例中基類和派生類都包含了一個標志,表示對象當前的銷毀狀態。這是種防御性手段,各個對象維持自身的狀態可以把銷毀過程中可能出現的錯誤限制在了一個類型中,而不會影響到組成對象的所有類型。Dispose()方法可以被調用多次,即使對象已經被銷毀,終結器也有類似的規則。
同時我們應該看到,示例程序中的兩個類並沒有提供終結器,這是由於這里沒有使用非托管資源——因此不需要終結器(也就是說,上面的代碼一直會調用Dispose(true))。除非你的類中包含非托管資源,否則不應該實現終結器,因為這個會對性能造成很大的影響(即使終結器用於也不會被調用)。不過這個標准模式確實不可改變的,因為派生類中可能會使用非托管資源,所以添加終結器,進而實現Dispose(bool),以便正確處理非托管資源。
關於銷毀/清理方法最重要的建議:
- Dispose()方法只能釋放資源,不能再方法內執行任何別的操作
- 終結器除了清理非托管資源之外不應該有任何別的操作
2.提供終結器
如果你的類使用了非托管資源,那么你必須提供一個終結器。因為類的使用者可能會忘記調用Dispose()方法。如果沒有提供終結器,而使用者又忘記調用Dispose()的話,那么就會發生資源泄露。終結器是唯一可以保證能夠釋放掉非托管資源的方式,沒有之一。
我們通過調用Object.Finalize 方法來使用終結器,默認情況下,Finalize方法不會執行任何操作,如果我們想要讓GC在回收對象內存前執行清理非托管資源的操作,我們必須先在類中重寫該方法(添加析構函數)。
2.1 析構函數
在C#中不能夠直接重寫或調用Finalize(),只能通過析構函數語法來間接調用終結器。
析構函數是C#調用終結器的操作機制,析構函數是由GC來負責調用的。程序退出時也會調用析構函數。析構函數具有下面的幾個特點:
- 只能對類使用析構函數(結構不可以)。
- 一個類只能有一個析構函數。
- 無法繼承或重載析構函數。
- 無法調用析構函數。 它們是被自動調用的。
- 析構函數既沒有修飾符,也沒有參數。
我們看下面的使用析構函數的示例:
1 public class Employee 2 { 3 //析構函數 4 ~Employee() 5 { 6 //清理操作 7 } 8 }
經過編譯器編譯后會生成和下面類似的代碼:
1 public class Employee 2 { 3 protected override void Finalize() 4 { 5 try 6 { 7 //清理操作... 8 } 9 finally 10 { 11 base.Finalize(); 12 } 13 } 14 }
從上面的代碼我們知道:通過自動調用基類型的析構函數可以保證繼承鏈上的對象所使用的非托管資源得到有效的釋放。或者我們可以直接查看IL代碼:
在GC運行時,它會立即清理掉那些沒有提供終結器的垃圾對象,而提供了終結器的垃圾對象會停留在內存中,被添加到一個叫做“終結隊列“(finalization queue)的地方。GC會使用另一個線程來執行隊列中對象的終結。終結器完成工作之后,這些垃圾對象才能夠從內存中清理出去。從這里我們可以看出使用終結器會在很大程度上影響程序的性能。
小節
對於包含了非托管資源或者某個成員實現了IDisposable接口的類型必須為其提供一個終結器,即使需要的只是IDisposable接口,而不是終結器也需要實現完整的模式——同時提供終結器和實現IDisposable接口。否則派生類(可能包含非托管資源)就不得不在標志的Dispose模式之外自成體系,增加其復雜性,請遵守前面實現的標准Dispose模式,會節省你、你的類的使用者以及基於你的類型的派生類作者的大量時間。
參考&進一步閱讀