もう一歩、シンプルな実装をするため解放ヘルパーなんてどう?と生成AIにすすめられたので触れてみる
Idisposableのクラスに、object を格納するリストを作って、disposeで逆順に開放し、
GC.Collect();GC.WaitForPendingFinalizers();も最後にしっかり2度呼ぶという話
ついでに、workbookは、登録時に閉じるか否か、閉じる場合は保存するか否かと
applicationは、登録時に閉じるか否かを指定できるようにすると・・・
こんな感じのクラスになります
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
namespace SampleExcel
{
internal class ComReleaseManager : IDisposable
{
/// <summary>
/// COMオブジェクトデータ
/// </summary>
/// <param name="ComObject">COMオブジェクト</param>
/// <param name="ShouldClose">WorkbookでCloseするか否か, ApplicationでQuitするか否か</param>
/// <param name="SaveChanges">WorkbookでCloseする場合、保存するかしないか</param>
private record ComObjectEntry(object ComObject, bool ShouldClose, bool SaveChanges);
// new () は左辺の型で new するのを省略する書き方(C# 9.0)
private readonly List<ComObjectEntry> _comObjects = new ();
// where T : class で値型ではなく、参照型のみに限定している
private T RegisterInternal<T>(T comObject, bool shouldClose, bool saveChanges) where T : class
{
// null ならエラー
if (comObject == null)
{
throw new ArgumentNullException();
}
// COMオブジェクトデータとして登録
_comObjects.Add(new ComObjectEntry(comObject, shouldClose, saveChanges));
// COMオブジェクトを返却
return comObject;
}
// 一般的なCOMオブジェクト
public T Register<T>(T comObject) where T : class
{
return RegisterInternal(comObject, false, false);
}
// Application登録(shouldClose指定あり)
public Excel.Application Register(Excel.Application app, bool shouldClose)
{
return RegisterInternal(app, shouldClose, false);
}
// Workbook登録(shouldCloseとsaveChangesを指定)
public Excel.Workbook Register(Excel.Workbook workbook, bool shouldClose, bool saveChanges)
{
return RegisterInternal(workbook, shouldClose, saveChanges);
}
// 後処理
public void Dispose()
{
for (int i = _comObjects.Count - 1; i >= 0; i--)
{
var entry = _comObjects[i];
try
{
if (entry.ShouldClose)
{
switch (entry.ComObject)
{
case Excel.Workbook workbook:
workbook.Close(entry.SaveChanges);
break;
case Excel.Application app:
app.Quit();
break;
}
}
Marshal.ReleaseComObject(entry.ComObject);
}
catch
{
// ログなどに残してもよい(今回は無視)
}
}
_comObjects.Clear();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
もう少し拡張するなら、debugビルドかreleaseビルドかによって
Marshal.ReleaseComObject関数が返す参照カウントの値をログにだせるようにすると解放漏れがあった場合にはすぐにわかってよいかも
補足:recordについて
recordは、簡単に説明するとC#9.0に追加された機能 classでデータのみ保持する場合のシンタックスシュガーみたいなものになります以下コードは
public record Person(string Name, int Age);
C# 9.0より前では以下と一緒
public class Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public override bool Equals(object obj) =>
obj is Person other && Name == other.Name && Age == other.Age;
public override int GetHashCode() => HashCode.Combine(Name, Age);
public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }}";
}
以下ソースコードを解放ヘルパーを使って書き直してみます
Before:
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System;
using Excel = Microsoft.Office.Interop.Excel;
namespace SampleCode
{
internal class SampleExcel
{
static void Main(string[] args)
{
Excel.Application? application = null;
Excel.Workbooks? workbooks = null;
Excel.Workbook? workbook = null;
Excel.Sheets? worksheets = null;
Excel.Worksheet? worksheet = null;
try
{
// 実行ファイルのあるフォルダパス取得
var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
if (folderPath == null) { return; }
// Excelファイルパスを作成
var filePath = Path.Combine(folderPath, "sample.xlsx");
// Excelを開く
application = new Excel.Application();
application.Visible = true;
// ブックを開く
workbooks = application.Workbooks;
workbook = workbooks.Open(filePath);
// シートを取得
worksheets = workbook.Sheets;
for (var i = 1; i <= worksheets.Count; i++)
{
// 各シートを取得して名前を表示
worksheet = worksheets[i];
Console.WriteLine(worksheet.Name);
CleanUpComObject(ref worksheet);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
CleanUpComObject(ref worksheet);
CleanUpComObject(ref worksheets);
CleanUpComObject(ref workbook);
CleanUpComObject(ref workbooks);
CleanUpComObject(ref application);
// RCW強制解放(残留プロセス対策)
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
static void CleanUpComObject<T>(ref T comObject, bool shouldClose = true, bool saveChanges = false)
{
// フラグによってWorkbookはClose / ApplicationはQuitする
if (shouldClose && comObject != null)
{
// 型をチェックする
if (comObject is Microsoft.Office.Interop.Excel.Workbook workbook)
{
// Workbookの場合
workbook.Close(saveChanges);
}
else if (comObject is Microsoft.Office.Interop.Excel.Application application)
{
// Applicationの場合
application.Quit();
}
}
// Objectを解放
if (comObject != null && Marshal.IsComObject(comObject))
{
Marshal.ReleaseComObject(comObject);
comObject = default!;
}
}
}
}
After:
using SampleExcel;
using System;
using System.IO;
using System.Reflection;
using Excel = Microsoft.Office.Interop.Excel;
namespace SampleCode
{
internal class SampleExcel
{
static void Main(string[] args)
{
try
{
// 実行ファイルのあるフォルダパス取得
var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
if (folderPath == null) { return; }
// Excelファイルパスを作成
var filePath = Path.Combine(folderPath, "sample.xlsx");
using (var comManager = new ComReleaseManager())
{
// Excelを開く
var application = comManager.Register(new Excel.Application(), true);
// ブックを開く
var workbooks = comManager.Register(application.Workbooks);
var workbook = comManager.Register(workbooks.Open(filePath), true, false);
// シートを取得
var worksheets = comManager.Register(workbook.Sheets);
for(var i = 1; i < worksheets.Count; i++)
{
var worksheet = comManager.Register(worksheets[i]);
Console.WriteLine(worksheet.Name);
}
}
// using をぬけると生成したComオブジェクトを逆順に解放し、必要に応じて、Close Quitし、2回GCを実行する
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
現実的で読みやすいレベルのソースコードになりました!
ただ、解放クラスは使いまわしをしない方針のため、セルを数万行チェックするといったようなケースの場合
大量にリストにオブジェクトを抱えた状態になるので、あまりおすすめできないとおもいます
それらは、一時的変数で管理してすぐに廃棄していくのがベストだと思うので、うまく組み合わせて使っていくことが重要だと思います