Chapter 5 - Item 46 : Utilize using and try / finally for Resource Cleanup

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。