C#規範整理·資源管理和序列化

資源管理(尤爲是內存回收)曾經是程序員的噩夢,不過在.NET平臺上這個噩夢彷佛已經不復存在。CLR在後臺爲垃圾回收作了不少事情,使得咱們如今談起在.NET上進行開發時,都會說仍是new一個對象吧!回收?有垃圾回收器呢。其實並無這麼簡單。
  對象序列化是現代軟件開發中的一項重要技術,不管是本地存儲仍是遠程傳輸,都會使用序列化技術來保持對象狀態。


資源管理

1.顯式釋放資源需繼承接口IDisposable

C#中的每個類型都表明一種資源,而資源又分爲兩類:html

  • 託管資源 由CLR管理分配和釋放的資源,即從CLR裏new出來的對象。
  • 非託管資源 不受CLR管理的對象,如Windows內核對象,或者文件、數據庫鏈接、套接字、COM對象等。

若是咱們的類型使用到了非託管資源,或者須要顯式地釋放託管資源,那麼就須要讓類型繼承接口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();
  }

2.即便提供了顯式釋放方法,也應該在終結器中提供隱式清理

在標準的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);
}

3.Dispose方法應容許被屢次調用

一個類型的Dispose方法應該容許被屢次調用而不拋異常。鑑於這個緣由,類型內部維護了一個私有的布爾型變量disposed,以下所示:

private bool disposed=false;

在實際清理代碼的方法中,加入了以下的判斷語句:

if(disposed)
{    
return;
}

在//省略部分的代碼,方法的最後爲disposed賦值爲true:disposed=true;這意味着若是類型已經被清理過一次,那麼清理工做將再也不進行。對象被調用過Dispose方法,並不表示該對象已經被置爲null,且被垃圾回收機制回收過內存,已經完全不存在了。事實上,對象的引用可能還在。可是,對象被Dispose過,說明對象的正常狀態已經不存在了,此時若是調用對象公開的方法,應該會爲調用者拋出一個ObjectDisposedException。

4.在Dispose模式中應提取一個受保護的虛方法

真正實現IDisposable接口的Dispose方法並無作實際的清理工做,它實際上是調用了下面這個帶布爾參數且受保護的虛方法:

///<summary>
///非密封類修飾用protected virtual
///密封類修飾用private///</summary>
///<param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{        
//省略代碼
}

之因此提供這樣一個受保護的虛方法,是由於考慮了這個類型會被其餘類繼承的狀況。若是類型存在一個子類,子類也許會實現本身的Dispose模式。受保護的虛方法用來提醒子類:必須在實現本身的清理方法時注意到父類的清理工做,即子類須要在本身的釋放方法中調用base.Dispose方法。

若是不爲類型提供這個受保護的虛方法,頗有可能讓開發者設計子類的時候忽略掉父類的清理工做。因此,基於繼承體系的緣由,要爲類型的Dispose模式提供一個受保護的虛方法。

5.在Dispose模式中應區別對待託管資源和非託管資源

Dispose模式設計的思路基於:若是調用者顯式調用了Dispose方法,那麼類型就該循序漸進地將本身的資源所有釋放。若是調用者忘記調用Dispose方法了,那麼類型就假定本身的全部託管資源會所有交給垃圾回收器回收,因此不進行手工清理。理解了這一點,咱們就理解了爲何在Dispose方法中,虛方法傳入的參數是true,而在終結器中,虛方法傳入的參數是false。

6.具備可釋放字段的類型或擁有本機資源的類型應該是可釋放的

咱們將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接口。

7.及時釋放資源

不少人會注意到:垃圾回收機制自動爲咱們隱式地回收了資源(垃圾回收器會自動調用終結器),因而不由會問:爲何還要主動釋放資源呢?咱們來看如下這個例子:

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方法。幾乎在全部狀況下,咱們都沒必要調用此方法,由於垃圾回收器會負責調用它。

但在本實例中,爲了體會一下不及時回收資源的危害,因此進行了一次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」關鍵字。

8.必要時應將再也不使用的對象引用賦值爲null

在CLR託管的應用程序中,存在一個「根」的概念,類型的靜態字段、方法參數,以及局部變量均可以做爲「根」存在(值類型不能做爲「根」,只有引用類型的指針才能做爲「根」)。
當檢查到方法內的「根」時,若是發現沒有任何一個地方引用了局部變量,則無論是否已經顯式將其賦值爲null,都意味着該「根」已經被中止。而後,垃圾回收器會發現該根的引用爲空,同時標記該根可被釋放。

須要注意一下幾點

  1. 局部變量賦值爲null無心義,由於編譯器在編譯時就會過濾。
  2. 類型的靜態字段賦值爲null是有意義的。是由於類型的靜態字段一旦被建立,該「根」就一直存在。因此,垃圾回收器始終不會認爲它是一個垃圾。非靜態字段則不存在這個問題。

在實際工做中,一旦咱們感受到本身的靜態引用類型參數佔用的內存空間比較大,而且用完後不會再使用,即可以馬上將其賦值爲null。這也許並沒必要要,但這絕對是一個好習慣。試想在一個系統中那些時不時在類型中出現的靜態變量吧!它們就那樣靜靜地待在內存裏,一旦被建立,就永遠不會離開。或許咱們能夠專門爲此寫一個小建議,那就是:儘可能少用靜態變量。

序列化

1.爲無用字段標註不可序列化

序列化是指這樣一種技術:把對象轉變成流。相反的過程,咱們稱爲反序列化。在不少的場合都須要用到這項技術,例如:

  • 把對象保存到本地,在下次運行程序的時候,恢復這個對象。
  • 把對象傳到網絡中的另一臺終端上,而後在此終端還原這個對象。
  • 其餘的場合,如:把對象複製到系統的粘貼板中,而後用快捷鍵Ctrl+V恢復這個對象。

有如下幾方面的緣由,決定了要爲無用字段標註不可序列化:

  1. 節省空間。類型在序列化後每每會存儲到某個地方,如數據庫、硬盤或內存中,若是一個字段在反序列化後不須要保持狀態,那它就不該該被序列化,這會佔用寶貴的空間資源。
  2. 反序列化後字段信息已經沒有意義了。如Windows內核句柄,在反序列化後每每已經失去了意義,因此它就不該該被序列化。
  3. 字段由於業務上的緣由不容許被序列化。例如,明文密碼不該該被序列化後一同保存在文件中。
  4. 若是字段自己所對應的類型在代碼中未被設定爲可序列化,那它就該被標註不可序列化,不然運行時會拋出異常SerializationException。
[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。

2.利用定製特性減小可序列化的字段

特性(attribute)能夠聲明式地爲代碼中的目標元素添加註解。運行時能夠經過查詢這些託管模塊中的元數據信息,達到改變目標元素運行時行爲的目的。在System.Runtime.Serialization命名空間下,有4個這樣的特性,下面是MSDN上對它們的解釋:

  • OnDeserializedAttribute,當它應用於某方法時,會指定在對象反序列化後當即調用此方法。
  • OnDeserializingAttribute,當它應用於某方法時,會指定在反序列化對象時調用此方法。
  • OnSerializedAttribute,若是將對象圖應用於某方法,則應指定在序列化該對象圖後是否調用該方法。
  • OnSerializingAttribute,當它應用於某個方法時,會指定在對象序列化前調用此方法。

示例:

[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);  
  }
}

3.使用繼承ISerializable接口更靈活地控制序列化過程

除了利用特性Serializable以外,咱們還能夠注意到在序列化的應用中,經常會出現一個接口ISerializable。接口ISerializable的意義在於,若是特性Serializable,以及與其相配套的OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute、NonSerialized等特性不能徹底知足自定義序列化的要求,那就須要繼承ISerializable了。
例如咱們要將一個對象反序列化成爲另一個對象,就要都實現ISerializable接口,原理其實很簡單,那就是在一個對象的GetObjectData方法中處理序列化,在另外一個對象的受保護構造方法中反序列化。

4.實現ISerializable的子類型應負責父類的序列化

咱們將要實現的繼承自ISerializable的類型Employee有一個父類Person,假設Person沒有實現序列化,而如今子類Employee卻要求可以知足序列化的場景。不過很遺憾,序列化器沒有默認去處理Person類型對象,須要咱們在子類中受保護的構造方法和GetObjectData方法,爲它們加入父類字段的處理

總結

若有須要, 上一篇的《C#規範整理·泛型委託事件》也能夠看看!

相關文章
相關標籤/搜索