C#メモ ExcelのCOMオブジェクトの破棄処理を実行直後にしてみる

C#

なんか…メインプロセスを終了しないと、Excelのプロセスが残っちゃう。
ってか、実行するとどんどん増えてく。
たとえば、コントロールパネルでみてみるとこんな感じでプロセスが実行した分だけ残っちゃう。

これで困るのはこんな感じ。

  • いらないプロセスがどんどん増えてメモリを余計に使ってしまう
  • ファイルを開いて処理して閉じることを繰り返すような処理をする場合、Excel自体の設定の変更が無視される
    ※最初に作ったExcelのプロセスでファイルを開くことになるので設定がそのまま

はじめに用意したメソッドはこんな感じ。
ExcelアプリケーションをQuit()メソッドで終了すれば勝手にCOMオブジェクトを開放してくれると思ったけど…そうはいかないみたい。
特にGUIでEXCELを開いたり閉じたりするような処理をするときは、プロセスがずっと残っててEXCELファイルが読み取り専用になっちゃったりするから続けて処理ができない。

/// <summary>
/// EXCELのファイルを開いて閉じるだけのメソッド
/// </summary>
static void Function()
{
	string location = @"C:\Users\tetsuyanbo\Desktop\Sample.xlsx";	// Excelファイルの場所
	Microsoft.Office.Interop.Excel.Application application = null;
	Microsoft.Office.Interop.Excel.Workbooks workbooks = null;
	Microsoft.Office.Interop.Excel.Workbook workbook = null;
	try
	{
		application = new Microsoft.Office.Interop.Excel.Application();
		application.DisplayAlerts = false; // ダイアログが表示されるようなことがあると処理が止まってしまうのでダイアログは無視するよう設定を変更しておく
		workbooks = application.Workbooks;
		workbook = workbooks.Open(location);
	}
	catch(Exception exception)
	{
		Console.WriteLine(exception.Message);
	}
	finally
	{
		application.Quit();
	}
}

いろいろ調べてみるとMicrosoftのページに書いてあった…。

https://blogs.msdn.microsoft.com/office_client_development_support_blog/2012/02/09/office-5/

で、対策のポイントはこんな感じ。

  • Excelで作ったオブジェクトはSystem.Runtime.InteropServices.Marshal.ReleaseComObject()で破棄したあとにnullを設定する
  • CG.Collect()→GC.WaitForPendingFinalizers()→GC.Collect()でガベージコレクターの内容をクリアする
  • Quit()メソッドは他のCOMオブジェクトを開放した後に呼び出す
  • もう一回CG.Collect()→GC.WaitForPendingFinalizers()→GC.Collect()でガベージコレクターの内容をクリアする

で、コード。
今回はExcelのファイルを開いて閉じるだけの処理を5回繰り返すことにする。
まずは、Excelのファイルを開いて閉じるだけの処理をするメソッドはこんな感じ。

/// <summary>
/// EXCELのファイルを開いて閉じるだけのメソッド
/// </summary>
static void Function()
{
	string location = @"C:\Users\tetsuyanbo\Desktop\Sample.xlsx";	// Excelファイルの場所
	Microsoft.Office.Interop.Excel.Application application = null;
	Microsoft.Office.Interop.Excel.Workbooks workbooks = null;
	Microsoft.Office.Interop.Excel.Workbook workbook = null;
	try
	{
		application = new Microsoft.Office.Interop.Excel.Application();
		application.DisplayAlerts = false; // ダイアログが表示されるようなことがあると処理が止まってしまうのでダイアログは無視するよう設定を変更しておく
		workbooks = application.Workbooks;
		workbook = workbooks.Open(location);
	}
	catch(Exception exception)
	{
		Console.WriteLine(exception.Message);
	}
	finally
	{
		// 使用したことのあるCOMオブジェクトを最後に呼び出したものから開放する
		if(workbook != null)
		{
			System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook);
			workbook = null;
		}
		if(workbooks != null)
		{
			System.Runtime.InteropServices.Marshal.ReleaseComObject(workbooks);
			workbooks = null;
		}
		// ここで一度ガベージコレクションを実行する
		GC.Collect();
		GC.WaitForPendingFinalizers();
		GC.Collect();
		// EXCELアプリケーションのクローズはEXCELに関連するオブジェクトを開放してから実行してから
		// EXCELのCOMオブジェクトを開放する
		if(application != null)
		{
			application.Quit();
			System.Runtime.InteropServices.Marshal.ReleaseComObject(application);
			application = null;
		}
		// アプリケーションを終了するためにもう一度ガベージコレクションを実行する
		GC.Collect();
		GC.WaitForPendingFinalizers();
		GC.Collect();
	}
}

テスト用に作ったメソッドはこんな感じ。
5回繰り返す処理だけ。

/// <summary>
/// エントリーポイント
/// </summary>
/// <param name="arguments">コマンドライン引数(今回は使わない)</param>
static void Main(string[] arguments)
{
	try
	{
		// EXCELのファイルを開いて閉じるだけの処理を5回繰り返す
		// ※成功していればfinallyのところでタスクマネージャーを確認するとEXCELプロセスがないはずで
		//  失敗していればタスクマネージャーを確認すると5個のEXCELプロセスが残っちゃうはず...
		for (int current = 0; current < 5; current++)
		{
			Function();
		}
	}
	finally
	{
		// タスクマネージャーを確認するためにここで一度処理を止める
		Console.WriteLine(@"ここまできたらExcelのプロセスはないはず...続けるには何かキーを押すこと");
		Console.ReadKey();
	}
}

実行してみるとこんな感じ…コマンドプロンプトだけではわからん。

タスクマネージャーをみてみると、実行したときに前はプロセスが残ってたんだけど…残ってない!

そんなこんなで、明日への自分へのメモってことで。