C#中的每個類型都表明一種資源,而資源又分爲兩類:html
若是咱們的類型使用到了非託管資源,或者須要顯式地釋放託管資源,那麼就須要讓類型繼承接口IDisposable,這毫無例外。這至關於告訴調用者:類型對象是須要顯式釋放資源的,你須要調用類型的Dispose方法。,一個標準的繼承了IDisposable接口的類型應該像下面這樣去實現。這種實現咱們稱爲Dispose模式:程序員
public class SampleClass:IDisposable { //演示建立一個非託管資源 private IntPtr nativeResource=Marshal.AllocHGlobal(100); //演示建立一個託管資源 private AnotherResource managedResource=new AnotherResource(); private bool disposed=false; ///<summary> ///實現IDisposable中的Dispose方法 ///</summary> public void Dispose() { //必須爲true Dispose(true); //通知垃圾回收機制再也不調用終結器(析構器) GC.SuppressFinalize(this); } ///<summary> ///不是必要的,提供一個Close方法僅僅是爲了更符合其餘語言(如C++)的規範 ///</summary> public void Close() { Dispose(); } ///<summary> ///必需的,防止程序員忘記了顯式調用Dispose方法 ///</summary> ~SampleClass() { //必須爲false Dispose(false); } ///<summary> ///非密封類修飾用protected virtual ///密封類修飾用private ///</summary> ///<param name="disposing"></param> protected virtual void Dispose(bool disposing) { if(disposed) { return; } if(disposing) { //清理託管資源 if(managedResource!=null) { managedResource.Dispose(); managedResource=null; } } //清理非託管資源 if(nativeResource!=IntPtr.Zero) { Marshal.FreeHGlobal(nativeResource); nativeResource=IntPtr.Zero; } //讓類型知道本身已經被釋放 disposed=true; } public void SamplePublicMethod() { if(disposed) { throw new ObjectDisposedException("SampleClass","SampleClass is disposed"); } //省略 } }
若是類型須要顯式釋放資源,那麼必定要繼承IDispose接口。
承IDispose接口也爲實現語法糖using帶來了便利。在C#編碼中,若是像下面這樣使用using,編譯器會自動爲咱們生成調用Dispose方法的IL代碼:數據庫
using(SampleClass c1=new SampleClass()) { //省略 }
至關於網絡
SampleClass c1; try{ c1=new SampleClass(); //省略 } finally { c1.Dispose(); }
在標準的Dispose模式中,咱們注意到一個以~開頭的方法,以下所示:this
///<summary> ///必須,防止程序員忘記了顯式調用Dispose方法 ///</summary> ~SampleClass() { //必須爲false Dispose(false); }
這個方法叫作類型的終結器。提供終結器的意義在於:咱們不能奢望類型的調用者確定會主動調用Dispose方法,基於終結器會被垃圾回收器調用這個特色,它被用做資源釋放的補救措。編碼
對於沒有繼承IDisposable接口的類型對象,垃圾回收器則會直接釋放對象所佔用的內存;而對於實現了Dispose模式的類型,在每次建立對象的時候,CLR都會將該對象的一個指針放到終結列表中,垃圾回收器在回收該對象的內存前,會首先將終結列表中的指針放到一個freachable隊列中。同時,CLR還會分配專門的線程讀取freachable隊列,並調用對象的終結器,只有到這個時候,對象纔會真正被標識爲垃圾,而且在下一次進行垃圾回收時釋放對象佔用的內存。線程
能夠看到,實現了Dispose模式的類型對象,起碼要通過兩次垃圾回收才能真正地被回收掉,由於垃圾回收機制會首先安排CLR調用終結器。基於這個特色,若是咱們的類型提供了顯式釋放的方法來減小一次垃圾回收,同時也能夠在終結器中提供隱式清理,以免調用者忘記調用該方法而帶來的資源泄漏。設計
注意1 在有的文檔中,終結器也稱作析構器。指針
注意2 若是調用者已經調用Dispose方法進行了顯式地資源釋放,那麼,隱式釋放資源(也就是終結器)就沒有必要再運行了。
FCL中的類型GC提供了靜態方法SuppressFinalize來通知垃圾回收器這一點。注意查看Dispose方法:code
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
一個類型的Dispose方法應該容許被屢次調用而不拋異常。鑑於這個緣由,類型內部維護了一個私有的布爾型變量disposed,以下所示:
private bool disposed=false;
在實際清理代碼的方法中,加入了以下的判斷語句:
if(disposed) { return; }
在//省略部分的代碼,方法的最後爲disposed賦值爲true:disposed=true;這意味着若是類型已經被清理過一次,那麼清理工做將再也不進行。對象被調用過Dispose方法,並不表示該對象已經被置爲null,且被垃圾回收機制回收過內存,已經完全不存在了。事實上,對象的引用可能還在。可是,對象被Dispose過,說明對象的正常狀態已經不存在了,此時若是調用對象公開的方法,應該會爲調用者拋出一個ObjectDisposedException。
真正實現IDisposable接口的Dispose方法並無作實際的清理工做,它實際上是調用了下面這個帶布爾參數且受保護的虛方法:
///<summary> ///非密封類修飾用protected virtual ///密封類修飾用private///</summary> ///<param name="disposing"></param> protected virtual void Dispose(bool disposing) { //省略代碼 }
之因此提供這樣一個受保護的虛方法,是由於考慮了這個類型會被其餘類繼承的狀況。若是類型存在一個子類,子類也許會實現本身的Dispose模式。受保護的虛方法用來提醒子類:必須在實現本身的清理方法時注意到父類的清理工做,即子類須要在本身的釋放方法中調用base.Dispose方法。
若是不爲類型提供這個受保護的虛方法,頗有可能讓開發者設計子類的時候忽略掉父類的清理工做。因此,基於繼承體系的緣由,要爲類型的Dispose模式提供一個受保護的虛方法。
Dispose模式設計的思路基於:若是調用者顯式調用了Dispose方法,那麼類型就該循序漸進地將本身的資源所有釋放。若是調用者忘記調用Dispose方法了,那麼類型就假定本身的全部託管資源會所有交給垃圾回收器回收,因此不進行手工清理。理解了這一點,咱們就理解了爲何在Dispose方法中,虛方法傳入的參數是true,而在終結器中,虛方法傳入的參數是false。
咱們將C#中的類型分爲:普通類型和繼承了IDisposable接口的非普通類型。非普通類型除了那些包含託管資源的類型外,還包括類型自己也包含一個非普通類型的字段的類型。
在標準的Dispose模式中,咱們對非普通類型舉了一個例子:一個非普通類型AnotherResource。因爲AnotherResource是一個非普通類型,因此若是如今有這麼一個類型,它組合了AnotherResource,那麼它就應該繼承IDisposable接口,代碼以下所示:
class AnotherSampleClass:IDisposable { private AnotherResource managedResource=new AnotherResource(); private bool disposed=false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }
類型AnotherSampleClass雖然沒有包含任何顯式的非託管資源,可是因爲它自己包含了一個非普通類型,因此咱們仍舊必須爲它實現一個標準的Dispose模式。
除此之外,類型擁有本機資源(即非託管類型資源),它也應該繼承IDisposable接口。
不少人會注意到:垃圾回收機制自動爲咱們隱式地回收了資源(垃圾回收器會自動調用終結器),因而不由會問:爲何還要主動釋放資源呢?咱們來看如下這個例子:
private void buttonOpen_Click(object sender,EventArgs e) { FileStream fileStream=new FileStream(@"c:\test.txt",FileMode.Open); } private void buttonGC_Click(object sender,EventArgs e) { System.GC.Collect(); }
若是連續兩次單擊打開文件按鈕,系統就會報錯,以下所示:
IOException:文件"c:\test.txt" 正由另外一進程使用,所以該進程沒法訪問此文件。
如今來分析:在打開文件的方法中,方法執行完畢後,因爲局部變量fileStream在程序中已經沒有任何地方引用了,因此它會在下一次垃圾回收時被運行時標記爲垃圾。那麼,何時會進行下一次垃圾回收呢,或者說垃圾回收器何時纔開始真正進行回收工做呢?微軟官方的解釋是,當知足如下條件之一時將發生垃圾回收:
但在本實例中,爲了體會一下不及時回收資源的危害,因此進行了一次GC.Collect方法的調用,你們能夠仔細體會運行這個方法所帶來的不一樣。
垃圾回收機制中還有一個「代」的概念。一共分爲3代:0代、1代、2代。第0代包含一些短時間生存的對象,如示例代碼中的局部變量fileStream就是一個短時間生存對象。當buttonOpen_Click退出時,fileStream就被丟到了第0代,但此刻並不進行垃圾回收,當第0代滿了的時候,運行時會認爲如今低內存的條件已知足,那時纔會進行垃圾回收。因此,咱們永遠不知道fileStream這個對象(或者說資源)何時纔會被回收。在回收以前,它實際已經沒有用處,卻始終佔據着內存(或者說資源)不放,這對應用系統來講是一種極大的浪費,而且,這種浪費還會干擾程序的正常運行(如在本實例中,因爲它始終佔着文件資源,致使咱們不能再次使用這個文件資源了)。
不及時釋放資源還帶來另一個問題。在上面中咱們已經瞭解到,若是類型自己繼承了IDisposable接口,垃圾回收機制雖然會自動幫咱們釋放資源,可是這個過程卻延長了,由於它不是在一次回收中完成全部的清理工做。本實例中的代碼由於fileStream繼承了IDisposable接口,故第一次進行垃圾回收的時候,垃圾回收器會調用fileStream的終結器,而後等待下一次的垃圾回收,這時fileStream對象纔有可能被真正的回收掉。
瞭解了不及時釋放資源的危害後,如今來改進這個程序,以下所示:
private void buttonOpen_Click(object sender,EventArgs e) { FileStream fileStream=new FileStream(@"c:\test.txt",FileMode.Open); fileStream.Dispose(); }
這確實是一種改進,可是咱們沒考慮到方法中的第一行代碼可能會拋出異常。若是它拋出異常,那麼fileStream.Dispose()將永遠不會執行。因而,再一次改進,以下所示:
FileStream fileStream=null; try { fileStream=new FileStream(@"c:\test.txt",FileMode.Open); } finally { fileStream.Dispose(); }
爲了更進一步簡化語句,還可使用語法糖「using」關鍵字。
在CLR託管的應用程序中,存在一個「根」的概念,類型的靜態字段、方法參數,以及局部變量均可以做爲「根」存在(值類型不能做爲「根」,只有引用類型的指針才能做爲「根」)。
當檢查到方法內的「根」時,若是發現沒有任何一個地方引用了局部變量,則無論是否已經顯式將其賦值爲null,都意味着該「根」已經被中止。而後,垃圾回收器會發現該根的引用爲空,同時標記該根可被釋放。
須要注意一下幾點
在實際工做中,一旦咱們感受到本身的靜態引用類型參數佔用的內存空間比較大,而且用完後不會再使用,即可以馬上將其賦值爲null。這也許並沒必要要,但這絕對是一個好習慣。試想在一個系統中那些時不時在類型中出現的靜態變量吧!它們就那樣靜靜地待在內存裏,一旦被建立,就永遠不會離開。或許咱們能夠專門爲此寫一個小建議,那就是:儘可能少用靜態變量。
序列化是指這樣一種技術:把對象轉變成流。相反的過程,咱們稱爲反序列化。在不少的場合都須要用到這項技術,例如:
有如下幾方面的緣由,決定了要爲無用字段標註不可序列化:
[Serializable] class Person { [NonSerialized] private decimal salary; public decimal Salary { get { return salary; } set { salary=value; } } private string name; public int Age{get;set;} public string Name { get { return name; } set { name=value; } [field:NonSerialized] public event EventHandler NameChanged; }
注意
1.因爲屬性本質上是方法,因此不能將NonSerialized特性應用於屬性上,在標識某個屬性不能被序列化時,自動實現的屬性顯然已經不能使用。
2.要讓事件不能被序列化,需使用改進的特性語法field:NonSerialized。
特性(attribute)能夠聲明式地爲代碼中的目標元素添加註解。運行時能夠經過查詢這些託管模塊中的元數據信息,達到改變目標元素運行時行爲的目的。在System.Runtime.Serialization命名空間下,有4個這樣的特性,下面是MSDN上對它們的解釋:
示例:
[Serializable] class Person { public string FirstName; public string LastName; [NonSerialized] public string ChineseName; [OnDeserializedAttribute] void OnSerialized(StreamingContext context) { ChineseName=string.Format("{0}{1}",LastName,FirstName); } }
除了利用特性Serializable以外,咱們還能夠注意到在序列化的應用中,經常會出現一個接口ISerializable。接口ISerializable的意義在於,若是特性Serializable,以及與其相配套的OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute、NonSerialized等特性不能徹底知足自定義序列化的要求,那就須要繼承ISerializable了。
例如咱們要將一個對象反序列化成爲另一個對象,就要都實現ISerializable接口,原理其實很簡單,那就是在一個對象的GetObjectData方法中處理序列化,在另外一個對象的受保護構造方法中反序列化。
咱們將要實現的繼承自ISerializable的類型Employee有一個父類Person,假設Person沒有實現序列化,而如今子類Employee卻要求可以知足序列化的場景。不過很遺憾,序列化器沒有默認去處理Person類型對象,須要咱們在子類中受保護的構造方法和GetObjectData方法,爲它們加入父類字段的處理
若有須要, 上一篇的《C#規範整理·泛型委託事件》也能夠看看!