如何為我們自己的包含非託管資源的類型編寫資源管理代碼呢?在 .NET 中為我們提供了一種標準的銷毀非託管資源的模式,這個標準的模式能夠使使用者通過調用IDisposable介面正常釋放掉非託管資源,也能夠保證使用者在忘記釋放資源時使用終結器釋放。這個標準模式可以和GC配合,保證僅在最糟糕的情況下才調用終結器,儘可能的降低其帶來的效能影響。
閱讀目錄:
1.實現IDisposable介面
1.1 資源釋放的標準模式
2.提供終結器
2.1 解構函式
小節
參考閱讀&進一步閱讀
1.實現IDisposable介面
實現IDisposable介面是一種標準的做法,用來通知使用者和運行時系統該對象包含的資源需要及時釋放。IDisposable.Dispose()方法僅僅定義了一個方法:
1 public interface IDisposable2 {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 Employee2 { 3 //解構函式4 ~Employee()5 {6 //清理操作7 }8 }
經過編譯器編譯後會產生和下面類似的代碼:
1 public class Employee 2 { 3 protected override void Finalize() 4 { 5 try 6 { 7 //清理操作... 8 } 9 finally10 {11 base.Finalize();12 }13 }14 }
從上面的代碼我們知道:通過自動調用基底類型的解構函式可以保證繼承鏈上的對象所使用的非託管資源得到有效釋放。或者我們可以直接查看IL代碼:
在GC運行時,它會立即清理掉那些沒有提供終結器的垃圾對象,而提供了終結器的垃圾對象會停留在記憶體中,被添加到一個叫做“終結隊列“(finalization queue)的地方。GC會使用另一個線程來執行隊列中對象的終結。終結器完成工作之後,這些垃圾對象才能夠從記憶體中清理出去。從這裡我們可以看出使用終結器會在很大程度上影響程式的效能。
小節
對於包含了非託管資源或者某個成員實現了IDisposable介面的類型必須為其提供一個終結器,即使需要的只是IDisposable介面,而不是終結器也需要實現完整的模式——同時提供終結器和實現IDisposable介面。否則衍生類別(可能包含非託管資源)就不得不在標誌的Dispose模式之外自成體系,增加其複雜性,請遵守前面實現的標準Dispose模式,會節省你、你的類的使用者以及基於你的類型的衍生類別作者的大量時間。
參考&進一步閱讀
清理非託管資源
解構函式(C#編程指南)