# 原則15:使用 using 和 try/finally 清理資源
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
非托管資源類型必須使用 IDisposable 接口的 Dispose() 方法釋放。.NET 的這個規則使得釋放資源的職責是類型的使用者,而不是類型和系統。因此,任何時候你使用的類型有 Dispose() 方法,你就有責任調用 Dispose() 釋放資源。最好的方法來保證 Dispose() 被調用時使用 using 語句或 try/finally 塊。
所有包含非托管資源的類型都應該實現 IDisposable 接口。此外,如果你沒有恰當的回收這些類型,它們會被動創建析構函數。如果你忘記回收這些對象,這些非內存資源會在晚些時候析構函數被執行的時候釋放。這就會使得這些對象在內存待的時間更久,從而會使得應用程序因占用系統資源過多而變慢。
幸運的是, C# 語言設計者知道顯示釋放資源是一個很常見的操作。他們給語言添加了關鍵字使得會更容易。
```
public void ExecuteCommand(string connString, string commandString)
{
SqlConnection myConnection = new SqlConnection(connString);
SqlCommand mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
```
在這個例子中,有兩個可回收的對象沒有恰當地被清理:SqlConnection 和 SqlCommand 。這兩個對象會一直停留在內存中直到它們的析構函數被調用。(這兩個類都從 System.ComponentModel.Component 繼承析構函數。
通過在執行命令和連接結束的時候調用 Dispose 修復這個這個問題:
```
public void ExecuteCommand(string connString, string commandString)
{
SqlConnection myConnection = new SqlConnection(connString);
SqlCommand mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
mySqlCommand.Dispose();
myConnection.Dispose();
}
```
那樣處理就很好了,除非這個 SQL 命令執行拋出了異常。這時,上面例子的 Dispose() 就不會被執行。使用 using 語句可以保證 Dispose() 被調用。你使用 using 語句分配對象,C# 編譯器就會產生 try/finally 塊包含這些對象:
```
public void ExecuteCommand(string connString, string commandString)
{
using (SqlConnection myConnection = new SqlConnection(connString))
{
using (SqlCommand mySqlCommand = new SqlCommand(commandString, myConnection))
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
}
```
當你在函數使用一個可回收對象時, using 塊是最簡單方法確保對象被恰當回收。using 語句會產生 try/finally 塊包裹被分配的對象。下面兩段代碼的 IL 代碼是一樣的:
```
SqlConnection myConnection = null;
// Example Using clause:
using (myConnection = new SqlConnection(connString))
{
myConnection.Open();
}
// example Try / Catch block:
try
{
myConnection = new SqlConnection(connString);
myConnection.Open();
}
finally
{
myConnection.Dispose();
}
```
如果你對沒有實現 IDisposable 類型上使用 using 語句,C# 編譯器會報錯。例如:
```
// Does not compile:
// String is sealed, and does not support IDisposable.
using (string msg = "This is a message")
Console.WriteLine(msg);
```
using 語句對在編譯時期類型實現 IDisposable 接口才能正常工作。你不能對任意對象使用:
```
// Does not compile.
// Object does not support IDisposable.
using (object obj = Factory.CreateResource())
Console.WriteLine(obj.ToString());
```
快速保護方法是使用 as 語句可以轉換為安全可回收對象不管是否實現 IDisposable 接口:
```
// The correct fix.
// Object may or may not support IDisposable.
object obj = Factory.CreateResource();
using (obj as IDisposable)
Console.WriteLine(obj.ToString());
```
如果 obj 實現了 IDisposable ,using 語句會產生清理的代碼。否則,using 語句變為 using(null) 這樣是安全的而且不做任何處理。如果你還是不確定使用 using 塊包裹對象是否是正確的,為了安全:假設那樣做是正確的并且像前面的方法一樣使用 using 包裹對象。
這是只是介紹一個簡單的情況:當你在對象內使用可回收的局部對象,使用 using 語句包裹這個對象。現在看幾個復雜的用法。在第一個例子兩個不同對象需要回收:連接和命令。前面的做法是使用兩個 using 語句,將需要回收的兩個對象分別放在里面。每個 using 語句都會產生一個 try/finally 塊。等價于你寫了下面的代碼:
```
public void ExecuteCommand(string connString, string commandString)
{
SqlConnection myConnection = null;
SqlCommand mySqlCommand = null;
try
{
myConnection = new SqlConnection(connString);
try
{
mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (mySqlCommand != null)
mySqlCommand.Dispose();
}
}
finally
{
if (myConnection != null)
myConnection.Dispose();
}
}
```
每個 using 語句都會創建一個嵌套的 try/finally 塊。幸好,我們很少會在一個分配兩個實現 IDisposable 不同的對象。這種情況下,可以允許那樣,因為它能正常工作。但是,如果你要分配多個實現 IDisposable 的對象時,會是一個很糟糕的實現,我更喜歡自己寫 try/finally 塊:
```
public void ExecuteCommand(string connString, string commandString)
{
SqlConnection myConnection = null;
SqlCommand mySqlCommand = null;
try
{
myConnection = new SqlConnection(connString);
mySqlCommand = new SqlCommand(commandString,
myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (mySqlCommand != null)
mySqlCommand.Dispose();
if (myConnection != null)
myConnection.Dispose();
}
}
```
唯一可以拋棄這種寫法的理由是你能很容易的使用 using 和 as 語句實現:
這看上去很清晰,但是會有一個很狡猾的問題。 如果 SqlCommand() 構造拋出異常 SqlConnection 對象將不會被回收。 myConnection 已經被創建,但是當 SqlCommand 構造函數執行時代碼就不會進入 using 塊。在 using 塊沒有構造好, Dispose 的調用就會被跳過。你必須確保任何實現 IDisposable 的對象的分配要在 using 塊或 try 塊內。否則,資源泄露就會發生。
```
public void ExecuteCommand(string connString, string commandString)
{
// Bad idea. Potential resource leak lurks!
SqlConnection myConnection = new SqlConnection(connString);
SqlCommand mySqlCommand = new SqlCommand(commandString,myConnection);
using (myConnection as IDisposable)
using (mySqlCommand as IDisposable)
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
```
到目前為止,你已經學會了兩種最常見的情況了。當你在一個函數只分配一個可回收對象,使用 using 語句是最好方法確保資源被釋放。如果在一個方法中要分配多個對象,創建多個 using 塊或自己寫一個 try/finally 塊。
釋放不同的可回收對象會有一些細微的差別。有些類型同時支持 Dispose 方法和 Close 方法去釋放資源。 SqlConnection 就是其中之一。你像下面那樣可以關閉 SqlConnection :
```
public void ExecuteCommand(string connString, string commandString)
{
SqlConnection myConnection = null;
try
{
myConnection = new SqlConnection(connString);
SqlCommand mySqlCommand = new SqlCommand
(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (myConnection != null)
myConnection.Close();
}
}
```
這個版本是關閉連接,但跟回收它還是有一些不一樣。Dispose 方法不只是釋放資源:它還會告訴垃圾回收器這個對象不用執行析構函數。 Dispose 會調用 GC.SuppressFinalize() 。 Close 就沒有這樣的操作。結果,對象還待在析構隊列中,即使析構已經不需要。如果你可以選擇, Dispose 會比 Close 更好。你可以查看原則18了解所有細節。
Dispose() 不會將對象從內存中移除。它會觸發對象釋放非托管資源。這意味著你使用已經回收的對象會有問題。上面 SQLConnection 就是例子。 SQLConnection 的 Dispose() 方法斷開了數據庫的連接。在你回收這個連接之后,SQLConnection 還留著內存中,但是不再和數據庫保持連接。所以,在任何地方都不要回收還在被引用的對象。
在某些方面,C# 的資源管理會比 C++ 更困難。你不能依賴最終的析構函數清理所有使用到的資源。但是垃圾回收機制對你再簡單不過了。你使用到絕大多數類都沒有實現 IDisposable 。在 .Net 框架的1500多個類,只有少于100個類實現了 IDisposable 。當你使用到其中的類,記得回收它們。你可以將這些對象放在 using 塊或 try/finally 塊中。無論你使用哪一種方式,確保每次所有對象總是都被恰當釋放。
小結:
D.S.Qiu 一直都不知道 Dispose 的作用,并且也很疑惑 Close 和 Dispose 的區別(文末給予了解答,痛快)。但是對 Dispose 還是沒有吃透,C# 執行 Dispose 的內部流程是怎么樣的,都做了哪些操作?
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2078084](/blog/2078084)
更多精彩請關注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:使用動態接收匿名類型參數