原則18:實作标準的處理(Dispose)模式
2010年08月01日
我們已經讨論過,處理一個占用了非托管資源對象是很重要的。現在是時候來讨論如何寫代碼來管理這些類占用的非記憶體資源了。一個标準的模式就是利用.Net架構提供的方法處理非記憶體資源。你的使用者也希望你遵守這個标準的模式。也就是通過實作IDisposable接口來釋放非托管的資源,當然是在使用者記得調用它的時候,但如果使用者忘記了,析構函數也會被動的執行。它是和垃圾回收器一起工作的,確定在一些必要時候,你的對象隻會受到因析構函數而造成的性能損失。這正是管理非托管資源的好方法,是以有必要徹底的弄明白它。
處在類繼承關系中頂層的基類應該實作IDisposable接口來釋放資源。這個類型也應該添加一個析構函數,做為最後的被動機制。這兩個方法都應該是用虛方法來釋放資源,這樣可以讓它的派生類重載這個函數來釋放它們自己的資源。派生類隻有在它自己需要釋放資源時才重載這個函數,并且一定要記得調用基類的方法。
開始時,如果你的類使用了非記憶體資源,則一定得有一個析構函數。你不能指望你的使用者總是記得調用Dispose方法,否則當他們忘記時,你會丢失一些資源。這或許是因為他們沒有調用Dispose的錯誤,但你也有責任。唯一可以確定非記憶體資源可以恰當釋放的方法就是建立一個析構函數。是以,添加一個析構函數吧!
當垃圾回收器運作時,它會直接從記憶體中移除不用析構的垃圾對象。而其它有析構函數的對象還保留在記憶體中。這些對象被添加到一個析構隊列中,垃圾回收器會起動一個線程專門來析構這些對象。當析構線程完成它的工作後,這些垃圾對象就可以從記憶體中移除了。就是說,需要析構的對象比不需要析構的對象在記憶體中待的時間要長。但你沒得選擇。如果你是采用的這種被動模式,當你的類型占用非托管資源時,你就必須寫一個析構函數。但目前你還不用擔心性能問題,下一步就保證你的使用者使用更加簡單,而且可以避免因為析構函數而造成的性能損失。
實作IDisposable接口是一個标準的模式來告訴使用者和進行時系統:你的對象占有資源而且必須及時的釋放。IDisposable接口隻有一個方法:
public interface IDisposable
{
void Dispose();
}
實作IDisposable.Dispose()方法有責任完成下面的任務:
1、感覺所有的非托管資源。
2、感覺所有的托管資源(包括解除安裝一些事件)。
3、設定一個安全的标記來辨別對象已經被處理。如果在已經處理過的對象上調用任何方法時,你可以檢驗這個标記并且抛出一個ObjectDisposed的異常。
4、阻止析構。你要調用GC.SuppressFinalize(this)來完成最後的工作。
通過實作IDisposable接口,你寫成了兩件事:第一就是提供了一個機制來及時的釋放所有占用的托管資源(譯注:這裡就是指托管資源,當實作了這個接口後,可以通過調用Dispose來立即釋放托管資源),另一個就是你提供了一個标準的模式讓使用者來釋放非托管資源。這是十分重要的,當你在你的類型上實作了IDisposable接口以後,使用者就可以避免析構時的損失。你的類就成了.Net社群中表現相當良好的成員。
但在你建立的機制中還是存在一些漏洞。如何讓一個派生類清理自己的資源,同時還可以讓基類很好的再做資源清理呢?(譯注:因為調用Dispose方法時,必須調用基類的Dispose,當然是在基類有這個方法時。但前面說過,我們隻有一個标記來辨別對象是否處理過,不管先調用那個,總得有一個方法不能處理這個标記,而這就存在隐患) 如果基類重載了析構函數,或者自己添加實作了IDisposable接口,而這些方法又都是必須調用基類的方法的;否則,基類無法恰當的釋放資源。同樣,析構和處理共享了一些相同的職責:幾乎可以肯定你是複制了析構方法和處理方法之間的代碼。正如你會在原則26中學到的,重載接口的方法根本沒有如你所期望的那樣工作。Dispose标準模式中的第三個方法,通過一個受保護的輔助性虛函數,制造出它們的正常任務并且挂接到派生類來釋放資源。基類包含接口的核心代碼, 派生類提供的Dispose()虛函數或者析構函數來負責清理資源:
protected virtual void Dispose(bool isDisposing);
重載的方法同時完成析構和處理必須提供的任務,又因為它是虛函數,它為所有的派生類提供函數入口點。派生類可以重載這個函數,提供恰當的實作來釋放它自己的資源,并且調用基類的函數。當
isDisposing為true時你可能同時清理托管資源和非托管資源,當isDisposing為false時你隻能清理非托管資源。兩種情況下,都可以調用基類的Dispose(bool)方法讓它去清理它自己的資源。
當你實作這樣的模式時,這裡有一個簡單的例子。MyResourceHog 類展示了IDisposable的實作,一個析構函數,并且建立了一個虛的Dispose方法:
public class MyResourceHog : IDisposable
{
// Flag for already disposed
private bool _alreadyDisposed = false;
// finalizer:
// Call the virtual Dispose method.
~MyResourceHog()
{
Dispose(false);
}
// Implementation of IDisposable.
// Call the virtual Dispose method.
// Suppress Finalization.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(true);
}
// Virtual Dispose method
protected virtual void Dispose(bool isDisposing)
{
// Don't dispose more than once.
if (_alreadyDisposed)
return;
if (isDisposing)
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Set disposed flag:
_alreadyDisposed = true;
}
}
如果派生類有另外的清理任務,就讓它實作Dispose方法:
public class DerivedResourceHog : MyResourceHog
{
// Have its own disposed flag.
private bool _disposed = false;
protected override void Dispose(bool isDisposing)
{
// Don't dispose more than once.
if (_disposed)
return;
if (isDisposing)
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Let the base class free its resources.
// Base class is responsible for calling
// GC.SuppressFinalize()
base.Dispose(isDisposing);
// Set derived class disposed flag:
_disposed = true;
}
}
注和意,派生類和基類都有一個處理狀态的标記,這完全是被動的。重制的标記掩蓋了在處理時任何可能發生的錯誤,而且是單一的類型處理,而不是處理構成這個對象的所有類型。(譯注:就是基類與子類各自标記一個,互不影響。)
你應該被動的寫處理方法和析構函數,處理對象可能以任何順序發生,你可能會遇到這種情況:你的類中某個成員在你調用Dispose方法以前已經被處理過了。你沒有看到這種情況是因為Dispose()方法是可以多次調用的。如果在一個已經被處理過的對象上調用該方法,就什麼也不發生。析構函數也有同樣的規則。任何對象的引用存在于記憶體中時,你不用檢測null引用。然而,你引用的對象可能已經處理掉了,或者它已經析構了。
這就引入用了一個非常重要的忠告:對于任何與處理和資源清理相關的方法,你必須隻釋放資源! 不要在處理過程中添加其它任何的任務。你在處理和清理中添加其它任務時,可能會在對象的生存期中遇到一些嚴重而繁雜的問題。對象在你建立它時出生,在垃圾回收器認領它時死亡。你可以認為當你的程式不能再通路它們時,它們是睡眠的。你無法通路對象,無法調用對象的方法。種種迹象表明,它們就像是死的。但對象在宣布死亡前,析構函數還有最後一氣。析構函數什麼也不應該做,就是清理非托管資源。如果析構函數通過某些方法讓對象又變得可通路,那麼它就複活了。(譯注:析構函數不是使用者調用的,也不由.Net系統調用,而是在由GC産生的額外線程上運作的) 它又活了,但這并不好。即使是它是從睡眼中喚醒的。這裡有一個明顯的例子:
public class BadClass
{
// Store a reference to a global object:
private readonly ArrayList _finalizedList;
private string _msg;
public BadClass(ArrayList badList, string msg)
{
// cache the reference:
_finalizedList = badList;
_msg = (string)msg.Clone();
}
~BadClass()
{
// Add this object to the list.
// This object is reachable, no
// longer garbage. It's Back!
_finalizedList.Add(this);
}
}
當一個BadClass對象的析構函數執行時,它把自己的一個引用添加到了全局的連結清單中。這使得它自己又是可達的,它就又活了。前面向你介紹的這個方法會遇到一些讓人畏縮的難題。對象已經被析構了,是以垃圾回收器從此相信再也不用調用它的析構函數了。如果你實際要析構一個可達對象,這将不會成功。其次,你的一些資源可能不再有用。GC不再從記憶體上移除那些隻被析構隊列引用的對象,但它們可能已經析構了。如果是這樣,它們很可能已經不能使用了。(譯注:也就是說利用上面的那個方法讓對象複活後,很有可能對象是不可用的。)盡管BadClass所擁有的成員還在記憶體裡,它們像是可以被析構或者處理,但C#語言沒有一個方法可以讓你控制析構的次序,你不能讓這樣的結構可靠的運作。不要嘗試。
我還沒有看到這樣的代碼:用這樣明顯的方式來複活一個對象,除非是學術上的練習。但我看過這樣的代碼,析構函數試圖完成一些實質的工作,最後還通過析構函數的調用把引用放到對象中,進而把自己複活。析構函數裡面的代碼看上去是精心設計的,另外還有處理函數裡的。再檢查一遍,這些代碼是做了其它事情,而不是釋放資源!這些行為會為你的應用程式在後期的運作中産生很多BUG。删除這些方法,確定析構函數和Dispose()方法除了清理資源外,什麼也不做。
在托管環境裡,你不用為每一個建立的類寫析構函數;隻有需要釋放一些使用的非托管資源時才添加,或者你的類所包含的成員有實作了IDisposable接口的時候也要添加。即使如此,你也隻用實作IDisposable接口完成所有的功能就行了,不用析構函數。否則,你會限制你的派生類實作實作标準的Dispose習慣。 遵守這個我所講叙的标準的Dispose習慣。這會讓你的程式生活變得輕松,也為你的使用者,也為那些從你的類建立派生類的人。