Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得
在 .NET 運行環境下,雖然有 GC 負責託管資源的垃圾回收;但在非託管資源的情況下,該資源仍須開發者自行釋放。Item 17 提過的 IDisposable 介面的 Dispose 方法,需要使用者自行呼叫;雖然可以定義 Finalizer 做最後一道防線,但交由 Finalizer 釋放資源效率很低,此舉延長了記憶體垃圾存活時間。
.NET 針對上述情況,提供了兩種方式讓程式自行呼叫 Dispose。分別為 using 區塊和 try / finally 區塊。以下將做簡單介紹。
不好的寫法:
public void ExecuteCommand(string connectionString, string commandString)
{
var myConnection = new SqlConnection(connectionString);
var mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
沒有主動呼叫 myConnection 和 mySqlCommand 的 Dispose 方法,全權交由 Finalizer 呼叫;效率很低。
不好的寫法 2:
public void ExecuteCommand2(string connectionString, string commandString)
{
var myConnection = new SqlConnection(connectionString);
var mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
mySqlCommand.Dispose();
myConnection.Dispose();
}
雖然有主動呼叫 Dispose 方法,但若初始化 mySqlCommand 擲出例外時,因 myConnection 此時已經建構完成,將沒有機會呼叫 myConnection 的 Dispose 方法。
較好的寫法:
public void ExecuteCommand3(string connectionString, string commandString)
{
using (var myConnection = new SqlConnection(connectionString))
{
using (var mySqlCommand = new SqlCommand(commandString, myConnection))
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
}
利用 using 區塊將需要被釋放的物件包起來,using 是 C# 提供的語法糖;在離開該區塊時,會自動呼叫物件的 Dispose 方法。
另外 using 區塊產生的程式碼等同於產生 try / finally 區塊。
範例:
var myConnection = default(SqlConnection);
using (myConnection = new SqlConnection(connectionString))
{
myConnection.Open();
}
等於
try
{
myConnection = new SqlConnection(connectionString);
myConnection.Open();
}
finally
{
myConnection.Dispose();
}
需注意的是,using 區塊成立與否在於編譯期間,該物件是否實作 IDisposable 介面。並非所有物件都可以使用 using 區塊。
例如:
// 編譯失敗,string 為密封型別且沒有實作 IDisposable。
using (var msg = "This is a message")
Debug.WriteLine(msg);
// 編譯失敗,object 並沒有實作 IDisposable
using (var obj = Factory.CreateResource())
Debug.WriteLine(obj.ToString());
如果不能在編譯期間就確定該物件是否有實作 IDisposable,可以利用 as 方式試著轉型。
// 讓程式運行時,動態判斷 obj 是否有實作 IDisposable。
// 若有,離開 using 區塊時呼叫 Dispose。
// 若無,程式也不會擲出例外(相當於 using(null))
var obj = Factory.CreateResource();
using (obj as IDisposable)
Debug.WriteLine(obj.ToString()); // using(null) 時也會執行。
當我們有一個以上的 using 區塊時,編譯器會自動產生巢狀的 try / finally 區塊結構。
public void ExecuteCommand4(string connectionString, string commandString)
{
var myConnection = default(SqlConnection);
var mySqlCommand = default(SqlCommand);
try
{
myConnection = new SqlConnection(connectionString);
try
{
mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
mySqlCommand?.Dispose();
}
}
finally
{
myConnection?.Dispose();
}
}
遇到這種情況,可以自行將兩者結合成同一個 try / finally 區塊。
public void ExecuteCommand5(string connectionString, string commandString)
{
var myConnection = default(SqlConnection);
var mySqlCommand = default(SqlCommand);
try
{
myConnection = new SqlConnection(connectionString);
mySqlCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
// 需注意呼叫順序,LIFO。
finally
{
mySqlCommand?.Dispose();
myConnection?.Dispose();
}
}
然而,不要嘗試以下寫法。
public void ExecuteCommand6(string connectionString, string commandString)
{
// 記憶體有可能會洩漏。
// 當初始化 SqlCommand 擲出例外時,myConnection 資源將無法正確被釋放。
var myConnection = new SqlConnection(connectionString);
var mySqlCommand = new SqlCommand(commandString, myConnection);
using (myConnection as IDisposable)
using (mySqlCommand as IDisposable)
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
最後,有時會遇到類別同時提供兩種方法:Dispose 和 Close。兩者時常搞混,作者給的建議是,若是要明確的釋放資源(資源不再可用)就呼叫 Dispose,因為 Dispose 也必定呼叫了 GC.SuppressFinzlize(this);而 Close 則不一定會呼叫(或許只是代表先關閉,稍後此物件還能繼續使用)。為了避免模稜兩可,在需要非託管資源釋放的情境下,呼叫 Dispose 是比較好選擇。
由於 Dispose 只是代表釋放非託管資源,並非馬上回收物件記憶體(和 C++ Finalizer 不同,C++ 是立刻回收記體)。在 CLR 運行環境下,還需要等到該物件沒有任何根參考且下一次 GC 才會回收。也就是說,在對已經呼叫 Dispose 的物件做操作,有可能會發生未預期的例外狀況。
只有在確保物件不再需要使用時,才呼叫 Dispose。
1. 正確的使用 using 或 try / finally 區塊,主動的呼叫 Dispose。
2. 確保物件不再被使用時,才呼叫 Dispose。