Effective C# 46.以 using 與 try/final 清理資料 Effective C# 46.以 using 與 try/final 清理資料 (Utilize using and try/finally for Resource Cleanup)

Published on Monday, October 28, 2024

這個做法在之前也提到過很多次了,就是要用 using 或是 try/final 來確保資源一定能正確釋放掉。

例如下面這個方法建立兩個物件 myConnection 與 mySqlCommand,並且它們又都是有實做 IDisposable, 所以這段程式碼只有使用資源並沒有釋放資源的流程。

public void ExecuteCommand(string connString, string commandString)
{
	var myConnection = new SqlConnection(connString);
	var mySqlCommand = new SqlCommand(commandString, myConnection);
	myConnection.Open();
	mySqlCommand.ExecuteNonQuery();
}

你可能會這樣直接結束後呼叫 Dispose 方法來釋放掉對應的物件,但這樣的處理方式並不是最正確的,因為假如 mySqlCommand 執行中發生異常 那麼下面那兩個 Dispose 方法就會被跳過不會執行了。

public void ExecuteCommand(string connString, string commandString)
{
	var myConnection = new SqlConnection(connString);
	var mySqlCommand = new SqlCommand(commandString, myConnection);
	myConnection.Open();
	mySqlCommand.ExecuteNonQuery();
	mySqlCommand.Dispose();
	myConnection.Dispose();
}

使用 using 關鍵字才是比較好的處理方式,因為它實際上內部是利用 try/final 把程式碼包裹起來,確保把呼叫 Dispose 方法的邏輯放在 final 裡面保證釋放邏輯一定會被執行。

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 關鍵字之後編譯器會生成下面 try/final 的程式碼,由此可知我們也可以自己寫 try/final 也能達到跟使用 using 同樣的效果。

SqlConnection myConnection = null;
using (myConnection = new SqlConnection(connString))
{
   myConnection.Open();
}

try
{
   myConnection = new SqlConnection(connString);
   myConnection.Open();
}
finally
{
   myConnection.Dispose();
}

另外如果不確認物件是否有時做 IDisposable 可以先透過 as 進行檢查,using 內部會自行判斷是否要生成 try/final 的程式碼, 假設結果是 using(null) 則不會產生任何效果,程式碼也會繼續運行。

object obj = Factory.CreateResource();
using (obj as IDisposable)
   Console.WriteLine(obj.ToString());

上面提到的都是簡單的例子,假設今天有兩個物件用 using 關鍵字那麼就會產生出下面這種嵌套式的 try/final 程式碼。

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();
   }
}

這種時候就能自己寫 try/final 處理釋放問題,這樣寫可讀性也比較高。

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();
   }
}

但要且記不要寫出這樣的程式碼,假如 SqlCommand 在建構函式執行的過程中發生異常那麼 SqlConnection 物件就不會被釋放了, 所以關鍵是要把建構物件的流程也包含在 try 裡面才能避免這種問題。

public void ExecuteCommand(string connString, string commandString)
{
   SqlConnection myConnection = new SqlConnection(connString);
   SqlCommand mySqlCommand = new SqlCommand(commandString, myConnection);
   using (myConnection as IDisposable)
   using (mySqlCommand as IDisposable)
   {
       myConnection.Open();
       mySqlCommand.ExecuteNonQuery();
   }
}

最後要注意 Dispose Patter 有建議除了實做 Dispose 方法外,最好也同時實做一個擁有相同功能的 Close 方法,因為在某些場合用 Close 來代表釋放語意會比較通順,例如在 SqlConnection 這種連線開啟就是用 Open 關閉的時候搭配 Close 語意就比使用 Dispose 還通順, 所有有些 library 會同時提供這兩種釋放的方法,不過由於一定會提供 Dispose 方法而 Close 方法是看個人, 所以要額外注意呼叫 Close 前一定要確保兩邊的邏輯是相同的,以免有些資源在 Close 方法不會被釋放。


Summary

這個做法再次提出 using 的重要性,以及可以直接使用 try/final 實現自己的釋放流程,就能跳過 using 關鍵字幫忙產生的程式碼。