Effective C#筆記(2)

 C#採用GC(垃圾回收器)來管理內存,GC在它獨自的線程上運行。但是GC只管理內存,而不會翻譯其它的資源。在C++的時候,我們可以在析構函數裏面來釋放資源,但在C#中,因爲我們沒有辦法確定對象是什麼時候回收的,其析構函數的調用時間並不是可預期的,因此在析構函數裏面翻譯資源並不是一個很好的辦法。
另外,如果我們在析構函數裏面回收資源,由於GC在回收內存前必須調用其析構函數,因此GC必須另起一個線程來調用析構函數,該對象佔用的內在也只能在“下一次”GC回收內存的時候被回收。之所以用“下一次”,是因爲其並不一定是下一次,很有可能是很久以後。
這涉及到GC回收內存的時候一個機制:.NET GC定義了代來優化資源回收。“代”表示了對象有可能成爲垃圾的程度。剛創建的對象是0代,經過一次垃圾回收的是1代,經過兩次垃圾回收的是2代。0代的對象最有可能是垃圾,因爲是剛創建的,比較可能是局部變量;1代和2代對象有可能是類變量或者全局變量,比較不可能是垃圾。0代對象在每次垃圾回收都會檢查,1代對象大概是10次垃圾回收才檢查一次,2代對象大概100次垃圾回收才檢查一次。再次回到剛纔的析構函數,如果採用析構函數,則該對象在第一次垃圾回收的時候並不能立刻回收,因此會成爲1代對象,最起碼要大概10次垃圾回收的時候纔會再檢查一次,如果這時候析構函數還沒有執行完,則要100次垃圾回收後纔會去檢查。
當然,在C#中我們可以實現IDispose接口來實現這種需要自己回收資源的對象。

 (1) 儘量使用變量初始化而不是賦值。

如果一個類的所有的構造函數都要將變量的值初始化到一個值,則可以在聲明變量的時候進行初始化。這樣可以保證該變量在所有的構造函數裏面都被初始化。變量初始化是在所有構造函數執行前執行的。
如 public class A {priavate ArrayList _coll = new ArrayList(); }保證了_coll變量在所有的類實例中都被初始化了。
但有三種情況下是不能使用變量初始化的
(a) 如果變量初始值爲0或者null。這些在變量聲明的時候就已經自動初始化爲0了,用戶再初始化會降低效率。特別是對於值類型的變量。比如 MyValueType _myValue;這樣聲明就已經將這個變量的值初始化爲0了。而如果這樣聲明:MyValueType _myValue = new MyValueType();這樣是使用中間語言來初始化這個變量了,造成box和unbox的問題,效率是不高的。
(b) 如果一個變量在不同的構造函數被初始成的值是不同的,則要在每個構造函數裏面進行單獨的初始化,不然有些值會被初始化兩次。如
public class MyClass
{
    private ArrayList _coll = new ArrayList();
    MyClass() {}
    MyClass(int size) { _coll = new ArrayList( size ); }
}
則通過編譯器生成的代碼會類似如下(這當然是我們所不喜歡的):
public class MyClass
{
    private ArrayList _coll;
    MyClass() { _coll = new ArrayList(); }
    MyClass(int size)
    { 
        _coll = new ArrayList();  
        _coll = new ArrayList(size); 
    }
}
(c) 如果變量初始化的時候要處理異常的。因爲聲明變量的時候初始化並沒有辦法處理異常。

 (2) 靜態構造函數

靜態構造函數在靜態變量的初始化調用之後。靜態構造函數可以初始化靜態變量,實現Singleton。

(3) 使用構造函數的嵌套來重構構造函數的代碼

如果多個構造函數存在公共的代碼,應該採用構造函數的嵌套來實現重構,而不是通過公共的幫助函數來實現重構。雖然在代碼的執行效率上是一樣的(注:書上的說法有點錯誤,書上說代碼的效率會提高),但生成的代碼是比較乾淨的。比如:
public class MyClass
{
    public MyClass() { ComonFunc("Default"); }
    public MyClass(string val) { CommonFunc(val); }
}
當C#編譯器生成相應的代碼的時候,會生成大概如下的代碼:
public class MyClass
{
    public MyClass()
    {
        // 初始化變量
        // 調用基類的構造函數
        CommonFunc( "Default" );
    }
    public MyClass(string val)
    {
        // 初始化變量
        // 調用基類的構造函數
        CommonFunc( val );
    }
}
而如果我們在MyClass()裏面調用this("Default"),即調用另一個構造函數,則C#編譯器只會在MyClass(string val)裏面生成初始化變量和調用基類的構造函數的代碼,而不會在MyClass()裏面也生成重複的代碼。

 (4) Using 語句

如果對象實現了IDispose接口,要使用using語句來翻譯未託管的內存。使用using語句,C#編譯器會生成Try{}Finally{}語句來保證Dispose方法的調用。
將using語句使用在未實現IDispose接口的對象上,會產生編譯錯誤。如果我們不能確定一個對象是否實現了IDispose接口,但又想保證其能夠正確地釋放所使用的資源,可以使用以下的方法:Object obj = ...; using (obj as IDispose) {...},如果obj實現了IDispose接口,則其資源能夠正確釋放;如果obj未實現IDispose接口,則生成using(null),這樣不會出現任何的問題,除了該語句不作任何事情以外。
如果我們在語句中使用到了嵌套的using,則可以使用自己寫的Try{}Finally{}語句,使得生成的代碼更簡單一些。比如 using (A a = new A()) { using (B b = new B()) {...} },會產生類似以下的語句
A a = null;
try {
    a = new A();
    B b = null;
    try {
        b = new B();
    } finally {
        if (b != null) b.Dispose();
    }
} finally {
    if (a != null) a.Dispose();
}
該語句生成嵌套的Try{}Finally{}語句,使得代碼稍顯複雜,可以自己重構成如下的Try{}Finally{}語句,代碼會簡單很多。
A a = null;
B b = null;
try {
    a = new A();
    b = new B();
} finally {
    if (b != null) b.Dispose();
    if (a != null) a.Dispose();
}

有些類可能既實現了Dispose方法,又實現了Close方法,比如SqlConnection就實現了這兩種方法。如果我們寫成如下的語句:
try {
    sqlConnection = new SqlConnection();
} finally {
    if (sqlConnection != null) sqlConnection.Close();
}
雖然SqlConnection的連接也能夠被正確關閉,但是Close()方法和Dispose()所做的事情是不同的。Dispose做了更多的事情:Dispose方法通知GC這個對象已經不需要再調用“析構函數”了,因此GC在回收資源的時候,可以直接將這個對象所用的內在回收。但如果只用了Close方法,則GC在回收資源的時候仍然需要調用其“析構函數”,並暫時將其對象放入析構隊列中。還記得這種方法的壞處嗎?上面提過。因此要使用Dispose方法來釋放資源,如果這個對象實現了IDispose接口的話。

(5) 減少內存垃圾

雖然GC的功能很強大,但是我們也要儘量在寫代碼的時候減少內存的垃圾
(a) 可以考慮把一些經常被調用的函數裏面的引用類型的變量提升爲類變量,如果這些引用類型的變量值是固定的話。比如如果在一個OnPaint函數裏面創建Font myFont = new Font("Arial", 10.0f),因爲OnPaint函數是Windows經常調用的函數,而myFont是一個固定的引用類型的變量,這種情況下就可以將myFont定義成類的變量。但要記住如果把實現了IDispose的變量提升爲類變量,則需要爲這個類實現IDispose接口以正確釋放資源。
(b) 可以考慮爲類提供一些常用的Singleton的實例,這樣應用程序在使用到這些常用的實例的時候,就不需要重新創建一個將成爲垃圾的實例了。比如Brush類就提供了很多常用的Singleton實例,比如Brush.Black.
(c) 爲不可變的類型提供構建類。比如String就是一個不可變類,.NET提供了StringBuilder來構建String實例。

(6) 減少Boxing和UnBoxing的操作

在.NET中,因爲所有的類的基類都是System.Object,但值類型不是多態的,這兩者產生了矛盾。.NET採用了Boxing和UnBoxing(我不知如何翻譯)來解決這個問題,當在需要System.Object的對象中遇到值類型時,.NET會在堆上分配一個內存,用來存儲值類型對象,該內存是引用類型的,這樣就叫Boxing;當程序需要對該內存上的對象進行訪問時,會拷貝一個新的備份進行訪問,這樣就叫UnBoxing。Boxing和UnBoxing會產生性能上的問題,同時由於產生臨時的拷貝對象,會產生一些微妙的Bug。

要儘量避免Boxing和UnBoxing的操作。如在以下的代碼Console.WriteLine("{0}", 5);就會產生Boxing和UnBoxing的操作,因爲5是一個值類型的對象,而Console.WriteLine需要的對象是System.Object。.NET編譯器會產生類似以下的代碼:
int i = 5;
object o = i;
Console.WriteLine (o.ToString());
在o對象上調用ToString會產生UnBoxing的操作:
object o;
int i = (int)o;
string output = i.ToString();
而如果我們寫成這樣Console.WriteLine("{0}", 5.ToString());則可以很好地避免Boxing和UnBoxing的操作。

另一個很容易產生這種Boxing和UnBoxing的操作的地方是使用.Net1.x裏面的集合對象時,因爲這些集合對象都是接受System.Object對象的,因此值類型會產生Boxing和UnBoxing的操作。而操作這些集合對象中的元素果,要特別留意其取得的值是原來值的一份拷貝。比如ArrayList list; list[0]取得的是值類型的一份拷貝,在其上的操作都不會作用到ArrayList存儲的對象。我們可以採用繼承接口的方法來使得從集合對象中取出的對象不是拷貝,而是實際的引用。比如:
public interface IPersonName { string Name {get; set;} }
struct Person: IPersonName { ... }
這樣ArrayList list; 當存儲Person對象時,((IPersonName)list[0]).Name = "New Value"實際上是會將值作用在ArrayList中的元素的。因爲在Boxing操作時,其產生的引用類型對象會實現值類型的所有接口操作,因此對這些接口進行操作時是不需要UnBoxing操作的。

(6) 實現標準的Dispose模式

如果類裏面使用了未託管的資源,則需要爲該類實現IDisposable接口。該接口只有一個函數Dispose,在該函數裏面釋放未託管的資源,但同時我們也要實現析構函數,以防止用戶使用的時候忘記調用Dispose方法。該Dispose函數應該完成以下的四個目的:
(a) 釋放未託管的資源
(b) 翻譯託管的資源,包括刪除事件等
(c) 設置一個標記,說明該對象的Dispose函數已經被調用了。因爲Dispose函數可能會被調用多次,但只有第一次調用的時候才需要真正釋放資源
(d) 壓制析構函數的調用,GC.SuppressFinalize(true)。這樣GC在回收資源的時候就不會去調用其析構函數了,該對象佔用的內在資源能夠被很快地釋放掉。

以下是Dispose方法常用的實現模式:
public class MyResourceHog : IDisposable
{
    private bool _alreadyDisposed = false;
    ~MyResourceHog() { Dispose( false ); }
    public void Dispose() { Dispose ( true );  GC.SuppressFinalize( true ); }
    protected virtual void Dispose (bool isDisposing)
    {
        if (_alreadyDisposed) return;
        if (isDisposing) { //釋放託管的資源 }
        //釋放未託管的資源
        _alreadyDisposed = true;
    }
}
之所以使用一個Virtual的Dispose(bool isDisposing)函數是爲了使繼承的類能夠更好地實現其Dispose和析構函數。如果繼承的類需要釋放其未託管的資源,只需要如下:
public class DerivedResourceHog : MyResourceHog
{
    private bool _disposed = false;
    protected virtual void Dispose (bool isDisposing)
    {
        if (_disposed) return;
        if (isDisposing) { //釋放託管的資源 }
        //釋放未託管的資源
        base.Dispose(isDisposing);
        _disposed = true;
    }   
}
之所以繼承的類也同時定義一個標記來表示該類是否已經被釋放了,其實是防禦性的做法:Duplicating the flag encapsulates any possible mistakes made while disposing of an object to only the one type, not all types that make up an object(還沒參透)。Dispose方法可能會被調用多次,並且不同對象的Dispose方法被調用的順序是無法預知的。

在Dispose方法和析構函數中只能做釋放資源的動作,而不應該再進行其它的邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章