# 第二章 .NET 資源管理
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
一個很簡單的事實,.NET 程序運行在托管的環境有對創建有效 C# 的設計有很大影響。想要利用這個環境的優勢需要你的思維從其他環境的轉換到 .NET 公共語言運行庫(CLR)上去。這意味著你需要理解 .NET 垃圾回收器。.NET 內存管理環境的概述對于理解本章的具體建議還是很有必要的,所以我們從這個概述開始。
垃圾回收器(GC)為你控制托管內存。不像自然環境,你不需要負責大多數的內存泄露,懸浮指針,未初始化指針,或者其他一些了內存管理問題。但是垃圾回收器沒那么神奇:你也需要自己清理。你要負責非托管資源,例如文件句柄,數據庫連接,GDI+對象,COM對象,和其他系統對象。此外你可能引起對象停留在內存比你想象的久因為你使用 event 處理或 delegate 創建它們之間的連接。查詢,它執行等待結果,也會引起對象保持引用時間比你期望的更長。在閉包中捕獲約束變量,并且這些約束變量會可以一直可訪問直到已經離開了這個結果的作用域。
好消息是:因為 GC 控制內存,明確的設計會更容易實現。無論是循環引用,還是簡單關系和復雜的 web 對象,都是更容易的。GC 的
標記和壓縮算法可以高效檢測到這些關系和完全移除不可達 web 對象。 GC 決定對象是否可達是通過對象樹形結構從根部開始漫游,而不是強制跟蹤每個對象的引用,COM 就是這樣的。 EntitySet 類提供了例子,它的算法簡化了對對象關系的判定。實體是從數據數據庫加載的對象集合。每個實體可能包含其他實體對象的引用。這些實體還可以能包含對其他實體的鏈接。就像關系數據庫的實體集模型,這些鏈接和引用都是可循環的。
有些引用是展示不同 EntitySet 的 web 對象。釋放內存是 GC 的責任。因為 .NET 框架設計者不需要釋放這些對象,web 對象引用的釋放的復雜性不會構成問題。不需要決定 web 對象的合理釋放次序,這是 GC 的工作。GC 的設計簡化了識別這類 web 對象為垃圾的問題。應用可以停止引用任何實體就是垃圾。垃圾回收器會知道是否這個實體仍然由應用中活著的對象可到達。應用中任何不可達到的對象都是垃圾。
垃圾回收器運行自己的線程去移除程序中沒有用的內存。它每次也會壓縮托管的堆內存。壓縮堆是通過移動活著的對象到一個托管的堆中以使得未使用的內存在一個連續的內存塊。圖2.1 就是垃圾回收前和后堆內存的截圖對比。GC 處理完所有空閑內存都被放在連續的塊中。

就像你剛學到的,內存管理(堆內存的管理)完全是垃圾回收器的責任。其他系統的資源必須有開發者管理:你和使用你的類的人。兩種機制幫助開發者控制的非托管資源的壽命:析構函數和 IDisposable 接口。析構函數是一個被動的機制確保你的對象總是有方式釋放非托管資源。析構函數會有很多缺點,所有你也可以實現 IDisposable 接口提供幾乎不入侵的方式及時返回資源給系統。
析構函數被垃圾回收器調用。它們會在對象變成垃圾之后某個時間被調用。你不知道什么時候調用。你只能知道的是如果某個時間被調用了你的對象就不可到達。這是跟 C++ 很大的不同,并且對你的設計有很重要的影響。要經驗的 C++ 程序員編寫類總是在構造函數分配重要資源然后在析構函數釋放:
```
// Good C++, bad C#:
class CriticalSection
{
// Constructor acquires the system resource.
public CriticalSection()
{
EnterCriticalSection();
}
// Destructor releases system resource.
~CriticalSection()
{
ExitCriticalSection();
}
private void ExitCriticalSection()
{
throw new NotImplementedException();
}
private void EnterCriticalSection()
{
throw new NotImplementedException();
}
}
// usage:
void Func()
{
// The lifetime of s controls access to
// the system resource.
CriticalSection s = new CriticalSection();
// Do work.
//...
// compiler generates call to destructor.
// code exits critical section.
}
```
通常的 C++ 習慣是保證資源回收是沒有異常。這在 C# 是行不通的,至少,不是同樣的方式。確定的析構函數不是 .NET 環境或 C# 語言的一部分。在 C# 語言嘗試強制 C++ 習慣的確定的析構函數不會很好的奏效。在 C# ,析構函數最后才執行,但它不會及時執行。在上面的例子中,代碼最后會退出臨界區,但是在 C# 中,函數退出后它不會退出臨界區。那會在后面確定的時間發生。你不可能知道什么時候。析構函數只是保證對象申請的非托管資源會最終釋放。但析構函數執行沒有確定的時間,所以你的設計和編碼實踐應該盡量減少析構函數的創建,同時也盡量減少析構函數的執行如果它存在的話。在本章中你將學習到什么時候你一定要創建析構函數,以及如何減少有析構函數的負面影響。
依賴于析構函數還會引入性能的損失。需要析構函數對象會被垃圾回收器消耗一部分性能。當 GC 發現對象是垃圾但是需要執行析構,它不能將對象理解從內存中移除。首先,它調用析構函數。析構函數不是在和垃圾回收器同一個線程中執行的。而是, GC 把每個等待析構的對象放進一個隊列中并且起另一個線程去執行所有析構函數。它會持續進行,把垃圾移除。在下一次 GC 循環時,已經被析構的對象就會被移除內存。圖2.2 顯示3種 GC 操作和不同內存使用。注意到需要析構的對象會留著內存多些循環周期。

這可能會讓你認為需要析構的對象會在內存比一般對象多待一個 GC 周期。我只是簡化地描述。它是比我描述的更復雜因為 GC 的設計決策。.NET 垃圾回收器定義代來優化工作。代幫助 GC 更快確定最有可能是垃圾的候選對象。從上次垃圾回收操作創建的對象都是0代對象。在一次 GC 操作后存活下來的就是1代對象。經過2次或更多 GC 操作存活的是2代對象。代的目的是區分局部變量和生命周期是整個應用的對象。0代的對象大多數都是局部對象。成員變量和全家變量很快會進入1代而且最后進入2代。
GC 通過現在檢查1代和2代對象的頻率來優化工作。每次 GC 循環都會檢查0代對象。粗略假設 GC 會10次檢查0代和1代對象。而要超過100次檢查所有對象。考慮析構以及它的開銷:需要析構的對象要比不需要析構的對象多待超過9個回收循環。如果仍然沒有被析構,它將進入2代。在2代,對象會生存上100個循環知道下次2代回收。我已經花了一些時間解釋為什么析構函數不是一個好的解決方案。但是,你仍需要釋放資源。解決這些問題你可以使用 IDisposable 接口和標準回收模式(查看本章原則17)。
在最后,記住托管環境垃圾回收器會負責內存管理,最大的好處是:內存泄露和其他指針相關的問題不再是你的問題。非內存資源你要強制創建析構函數來保證正確清理那些非內存資源。析構函數會比較大影響你程序的性能,但是你必須實現它以避免內存泄露。實現和使用 IDisposable 接口避免析構函數引入的垃圾回收的性能消耗。下一節將進入具體的原則,幫助您創建程序,更有效地利用環境。
小結:
終于翻譯完第二章了,不斷不斷堅持,還是覺得要完成,本來凌晨2:00就要結束第二章的戰斗的,后面狀態還是沒有調整過來,從昨天翻譯有20頁,雖然現在感覺印象還不是特別深刻,但是至少有用還是可以明確感受到。
附上第二章的目錄:
+ [原則12:選擇變量初始化語法(initializer)而不是賦值語句](/blog/1987663)
+ [原則13:使用恰當的方式對靜態成員進行初始化](/blog/2077189)
+ [原則14:減少重復的初始化邏輯](/blog/2078078)
+ [原則15:使用 using 和 try/finally 清理資源](/blog/2078084)
+ [原則16:避免創建不需要的對象](/blog/2078554)
+ [原則17:實現標準的 Dispose 模式](/blog/2078979)
+ [原則18:值類型和引用類型的區別](/blog/2079672)
+ [原則19:確保0是值類型的一個有效狀態](/blog/2079730)
+ [原則20:更傾向于使用不可變原子值類型](/blog/2079804)
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2079806](/blog/2079806)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable<T> 和 IComparer<T> 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數