もう一歩、シンプルな実装をするため解放ヘルパーなんてどう?と生成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);
            }
        }
    }
}

現実的で読みやすいレベルのソースコードになりました!

ただ、解放クラスは使いまわしをしない方針のため、セルを数万行チェックするといったようなケースの場合
大量にリストにオブジェクトを抱えた状態になるので、あまりおすすめできないとおもいます

それらは、一時的変数で管理してすぐに廃棄していくのがベストだと思うので、うまく組み合わせて使っていくことが重要だと思います

投稿日時: 2025-05-14 13:18:14
更新日時: 2025-05-14 13:51:14

解放をしない場合の挙動

適切な解放がされないと、プロセスが消えずに残ることになります。

実際に確認をしてみます
以下のように、applicationクラスのインスタンスを作成し、すぐ閉じます。

using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

internal class SampleExcel
{
    static void Main(string[] args)
    {
        Excel.Application? application = null;
        application = new Excel.Application();
        application.Quit();
    }
}

これでおきることは、new Excel.Application()を実行すると

バックグラウンドにExcelのプロセス現れます

 

プログラムではQuitして終了したようにみえても、プロセスには残り続けています

もし、定時実行をし続けようならば、実行するたびにプロセスがのこっていきます・・・

残ったプロセスは手動で消してください・・・

アプリケーションを表示した際のプロセスの挙動

application.VisibleをtrueにするとExcelが表示されます

using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

internal class SampleExcel
{
    static void Main(string[] args)
    {
        Excel.Application? application = null;
        application = new Excel.Application();
        application.Visible = true;
    }
}

この時の動きは、 new Excel.Application()で バックグラウンドとしてプロセスが作成され

その後、application.Visible = true でExcelが表示されアプリのプロセスに移動

↑バックグラウンドのプロセス一覧 ↑アプリのプロセス一覧

プログラムはここで終了となり、開かれているExcelを閉じると、再びバックグラウンドの方に移動します

パット見た感じアプリの一覧から消えるので、解放していなくても消えたようにみえるかもしれませんが、バックグラウンド側に残っています

一度でも参照したら解放する

たとえば、exeのあるフォルダに、3シートある sample.xlsxがあったとします
開いて、各シート名を出力してみます

using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System;
using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

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

            // ブックを開く
            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);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            if (worksheet != null) { Marshal.ReleaseComObject(worksheet); }
            if (worksheets != null) { Marshal.ReleaseComObject(worksheets); }
            if (workbook != null)
            {
                workbook.Close();
                Marshal.ReleaseComObject(workbook);
            }
            if (workbooks != null) { Marshal.ReleaseComObject(workbooks); }
            if (application != null)
            {
                application.Quit();
                Marshal.ReleaseComObject(application);
            }
        }
    }
}

これは、バックグラウンドにプロセスが残ります
一番最後のSheet3については、finallyで解放していますが、
Sheet1とSheet2は、参照しているのに解放していないので、プロセスが残ります

参照したら都度解放する必要があります

using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System;
using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

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

            // ブックを開く
            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);

                // 参照したら都度解放する
                Marshal.ReleaseComObject(worksheet);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            if (worksheet != null) { Marshal.ReleaseComObject(worksheet); }
            if (worksheets != null) { Marshal.ReleaseComObject(worksheets); }
            if (workbook != null)
            {
                workbook.Close();
                Marshal.ReleaseComObject(workbook);
            }
            if (workbooks != null) { Marshal.ReleaseComObject(workbooks); }
            if (application != null)
            {
                application.Quit();
                Marshal.ReleaseComObject(application);
            }
        }
    }
}

不要にReleaseComObjectを実行した場合

ループしてWorkSheetのオブジェクトを参照した場合、都度解放をしているのでループを抜けた場合、
参照カウントは0になっています。ただ、ReleaseComObjectを実行してもnullにはならないので、
finallyにきたら、さらにReleaseComObjectを実行することになります。

一般的には、不要にReleaseComObjectを実行するものではないといわれていますが、
それによって意図しない状態になったのを見たことがないのでなんともいえないです。

そのため、途中で開放した場合は、nullにしてしまうのがより良いと考えられているそうです

using System.IO;
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

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

            // ブックを開く
            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);

                // 参照したら都度解放する
                Marshal.ReleaseComObject(worksheet);
                worksheet = null;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            if (worksheet != null) { Marshal.ReleaseComObject(worksheet); }
            if (worksheets != null) { Marshal.ReleaseComObject(worksheets); }
            if (workbook != null)
            {
                workbook.Close();
                Marshal.ReleaseComObject(workbook);
            }
            if (workbooks != null) { Marshal.ReleaseComObject(workbooks); }
            if (application != null)
            {
                application.Quit();
                Marshal.ReleaseComObject(application);
            }
        }
    }
}

とはいえこれはよいことではあるけども、少しめんどくさい
そこで、解放処理や、Close、Quitをする処理をまとめると・・・

using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System;
using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

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

    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!;
        }
    }
}

もう少し、掘り下げておくとWorksheetのオブジェクトについては
for文のループ中でしかつかっていないので、その中で変数を作り格納しすぐ開放すればよいように思える

for (var i = 1; i <= worksheets.Count; i++)
{
    // 各シートを取得して名前を表示
    var worksheet = worksheets[i];
    Console.WriteLine(worksheet.Name);

    // 何かしらの処理

    CleanUpComObject(ref worksheet);
}

ただ、参照したあとに何かしらの処理で例外が発生したら、参照カウントが下げることができないので
オブジェクトを参照した直後から、解放するまではtry catchで囲わないといけないので、
あちことで変数を作って参照していたらtry catchだらけになることに注意

for (var i = 1; i <= worksheets.Count; i++)
{
    // 各シートを取得して名前を表示
    var worksheet = worksheets[i];
    try
    {
        Console.WriteLine(worksheet.Name);

        // 何かしらの処理

        CleanUpComObject(ref worksheet);
    }
    catch
    {
        throw;
    }
    finally
    {
        CleanUpComObject(ref worksheets);
    }
}

いろいろ調べてみるかぎり、より完全に解放するためには、GCを2度読んであげるとよいとされているそうです

using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System;
using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

internal class SampleExcel
{
    static void Main(string[] args)
    {
        Excel.Application? application = null;
        Excel.Workbooks? workbooks = null;
        Excel.Workbook? workbook = 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);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            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!;
        }
    }
}

2度呼ぶ理由についての生成AIの説明

✅ 結論から:なぜ2回 GC.Collect(); GC.WaitForPendingFinalizers(); が必要なのか?

これは、COMオブジェクトの参照がすべて解放されたことを確実にするために行われます。

📌 背景知識:COMオブジェクトとRCW(Runtime Callable Wrapper)

・.NET から COM オブジェクト(例:Excel)を使うとき、RCW という「仲介役」が自動的に生成されます。

・RCW は、.NET 側のガベージコレクションによって管理されますが、RCW自体がCOMオブジェクトの参照カウントを持っているため、RCWが解放されるまでCOMオブジェクトは解放されません。

🔄 なぜ2回GCするのか?

1回目の GC.Collect(); GC.WaitForPendingFinalizers(); で何が起きるか

・.NETでGC対象になったRCWオブジェクト(≒Excel COMオブジェクトのラッパー)を収集対象にする

・それにより、RCWのファイナライザ(最終処理)が実行される

・そのファイナライザの中で、COMオブジェクトへの参照が ReleaseComObject() などで解放される

🔁 しかし、ファイナライザが実行された直後にGCが実行されるとは限らないため、もう一度 GC をかけることで:

・ファイナライザによって解放されたRCW自体も収集され

・結果として RCWが保持していたCOMオブジェクトの参照カウントが0になり、COMオブジェクトも確実に解放される

⚠ よくある誤解ポイント

・RCWの解放自体は、明示的に Marshal.ReleaseComObject() を使うことでもできます。これを使えば、2回の GC は不要なこともあります。

・ただし、RCWを複数の変数で共有していたり、Excelオブジェクトの階層(Workbooks→Workbook→Worksheetなど)で参照が残っていたりすると、ReleaseComObjectだけでは不十分で、最終的にGCで回収させる必要が出るため、2回GCが有効になる。

とのこと

おまけ

生成AIさんこのコードで問題ない?

worksheetsをループ処理でシート名を表示するプログラム
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System;
using Excel = Microsoft.Office.Interop.Excel;

namespace SampleCode; // C#10~

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!;
        }
    }
}

worksheetは、for文でしか使っていないので、一時変数に格納して不要になればすぐ解放することも考えられるけど、
オブジェクト参照から不要になり解放するまでに、処理があって例外が発生する可能性があると都度try~catchを
書く必要があるから、コードが読みにくくなるのであえて書いていないけどどう?

ご提示の設計・実装方針は現実的かつ推奨されるパターンです。

COMオブジェクト(特にExcel)は、取得したすべてのオブジェクトを明示的に解放することが重要であり、

finallyブロックでまとめて後始末をするやり方は、例外発生時も含めてリソースリークを防ぐ観点からも適切です

ポイント整理

・for文内でworksheetを都度解放するのは理想的ですが、例外処理や可読性の観点から冗長になりやすいです。

・あなたのように、finallyでworksheetも含めてまとめて解放する方法は、例外が発生しても確実にリソースを解放できるため、実運用でよく使われています

・ただし、for文内でCleanUpComObject(ref worksheet)を呼んでいるため、ループのたびに前の参照を解放し、finallyでも念のため解放する形になっています。この重複は問題ありませんが、二重解放を防ぐため、CleanUpComObject内でnullチェックをしているのは良い実装です。

投稿日時: 2025-05-13 03:43:13
更新日時: 2025-05-24 00:20:24

ExcelVBA

SaveAsでファイルパスを指定する

Sub Sample()
    Dim BookObj     As Workbook
    Dim FilePath    As String
    
    '新規ブック作成
    Set BookObj = Workbooks.Add
    
    '保存するファイルパス
    FilePath = ThisWorkbook.Path & "\sample.xlsx"
    
    '名前つけて保存
    BookObj.SaveAs Filename:=FilePath
    
    Set BookObj = Nothing
End Sub

ただし、ファイルが既にある場合は「この場所に 'XXXX'という名前のファイルが既にあります。置き換えますか?」
と表示されるため、ファイルがある場合も上書きするのであれば、以下のようにします

Sub Sample()
    Dim BookObj     As Workbook
    Dim FilePath    As String
    
    '新規ブック作成
    Set BookObj = Workbooks.Add
    
    '保存するファイルパス
    
    'ファイルパス作成
    FilePath = ThisWorkbook.Path & "\sample.xls"
    
    '同一ファイルがあった場合の警告表示を無効とする
    Application.DisplayAlerts = False
    
    '名前つけて保存
    BookObj.SaveAs Filename:=FilePath
    
    '警告表示を有効に戻す
    Application.DisplayAlerts = True
    
    Set BookObj = Nothing
End Sub

もし、ファイルが既にある場合に上書きしたくない場合は事前にファイル存在チェックをして処理を止めればよいでしょう

Excel(COM)

using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;
using System.Reflection;
using System.IO;
using System;

namespace SampleCode; // C#10~

internal class SampleExcel
{
    static void Main(string[] args)
    {
        Excel.Application? application = null;
        Excel.Workbooks? workbooks = null;
        Excel.Workbook? workbook = 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.Add();

            // 警告表示を無効にする
            application.DisplayAlerts = false;

            // 名前つけて保存
            workbook.SaveAs2(filePath);

            // 警告表示を有効に戻す
            application.DisplayAlerts = false;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            CleanUpComObject(ref workbook, true, false);
            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!;
        }
    }
}

ClosedXML

ファイルパスと、Streamを使った方法

□ファイルパスで名前を付けて保存

using ClosedXML.Excel;
using System;
using System.IO;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleClosedXML
{
    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 workbook = new XLWorkbook())
            {
                // シートが一つもないブックは保存でエラーになるので追加
                var sheet = workbook.AddWorksheet("Sheet1");

                // 名前をつけて保存
                workbook.SaveAs(filePath);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

□FileStreamで名前を付けて保存

using ClosedXML.Excel;
using System;
using System.IO;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleClosedXML
{
    static void Main(string[] args)
    {
        var filePath = string.Empty;

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            filePath = Path.Combine(folderPath, "sample.xlsx");

            // ファイル出力用のストリームを作成(まだ中身は空のものが作られる)
            using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
            // メモリ上にワークブックを作成(この時点ではストリームとは未接続)
            using (var workbook = new XLWorkbook())
            {
                // シートが一つもないブックは保存でエラーになるので追加
                var sheet = workbook.AddWorksheet("Sheet1");

                // 作成したワークブックをストリームに保存(初めてファイルと紐づく)
                workbook.SaveAs(stream);
            }
        }
        catch (Exception ex)
        {
            // エラーが発生し、保存ができていなければ、空ファイルを作っただけなので、紛らわしいので消しておく
            if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
            {
                File.Delete(filePath);
            }

            Console.WriteLine(ex.Message);
        }
    }
}

EPPlus

□ファイルパスで名前を付けて保存

using OfficeOpenXml;
using System;
using System.IO;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");

            using (var package = new ExcelPackage())
            {
                // シートが一つもないブックは保存でエラーになるので追加
                var worksheet = package.Workbook.Worksheets.Add("sample");

                // 保存
                package.SaveAs(filePath);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

FileStreamを使った場合は、ClosedXMLと同じなのでそちらを参照

ExcelDataReader

読み取りのみのライブラリのため保存はありません

NPOI

NPOIは、ファイルパスをうけとれないので、FileStreamで処理する

□xls

using NPOI.HSSF.UserModel;
using System.Reflection;

var filePath = "";
try
{
    // 実行ファイルのあるフォルダパス取得
    var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
    if (folderPath == null) { return; }

    // Excelファイルパスを作成
    filePath = Path.Combine(folderPath, "sample.xls");
    
    using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
    using (var workbook = new HSSFWorkbook())
    {
        // シートが一つもないブックは保存でエラーになるので追加
        workbook.CreateSheet("Sheet1");

        // 作成したワークブックをストリームに保存
        workbook.Write(stream);
    }
}
catch (Exception ex)
{
    // エラーが発生し、保存ができていなければ、空ファイルを作っただけなので、紛らわしいので消しておく
    if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
    {
        File.Delete(filePath);
    }

    Console.WriteLine(ex.Message);
}

□xlsx

using NPOI.XSSF.UserModel;
using System;
using System.IO;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleNPOI_xlsx
{
    static void Main(string[] args)
    {
        var filePath = "";

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            filePath = Path.Combine(folderPath, "sample.xlsx");

            using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
            using (var workbook = new XSSFWorkbook())
            {
                // シートなしでも保存はできてしまう
                // sample.xlsxの一部の内容に問題が見つかりました。と出るのでシートは追加すること
                workbook.CreateSheet("Sheet1");

                // 作成したワークブックをストリームに保存
                workbook.Write(stream);
            }
        }
        catch (Exception ex)
        {
            // エラーが発生し、保存ができていなければ、空ファイルを作っただけなので、紛らわしいので消しておく
            if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
            {
                File.Delete(filePath);
            }

            Console.WriteLine(ex.Message);
        }
    }
}

□xls/xlsx対応版

Program.cs

using System;
using System.IO;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleNPOI
{
    static void Main(string[] args)
    {
        var filePath = "";

        try
        {   // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            filePath = Path.Combine(folderPath, "sample.xlsx");

            var extension = Path.GetExtension(filePath);

            // ブックを開く、読み取りモード、共有は読み取りのみOK
            using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
            using (var workbook = WorkbookFactory.Create(fileStream, extension))
            {
                // シートなしでも保存はできてしまう
                // sample.xlsxの一部の内容に問題が見つかりました。と出るのでシートは追加すること
                workbook.CreateSheet("Sheet1");

                // 作成したワークブックをストリームに保存
                workbook.Write(fileStream);
            }
        }
        catch (Exception ex)
        {
            // エラーが発生し、保存ができていなければ、空ファイルを作っただけなので、紛らわしいので消しておく
            if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
            {
                File.Delete(filePath);
            }

            Console.WriteLine(ex.Message);
        }
    }
}

WorkbookFactory.cs

using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.IO;

namespace SampleCode; // C#10

public class WorkbookFactory
{
    public static IWorkbook Create(Stream stream, string extension)
    {
        extension = extension.ToLower();

        return extension switch
        {
            ".xls" => new HSSFWorkbook(stream),
            ".xlsx" => new XSSFWorkbook(stream),
            _ => throw new NotSupportedException("対象外の拡張子です")
        };
    }

    public static IWorkbook Create(string extension)
    {
        extension = extension.ToLower();

        return extension switch
        {
            ".xls" => new HSSFWorkbook(),
            ".xlsx" => new XSSFWorkbook(),
            _ => throw new NotSupportedException("対象外の拡張子です")
        };
    }
}
投稿日時: 2025-05-11 10:19:11
更新日時: 2025-05-24 11:45:24

最近の投稿

最近のコメント

タグ

アーカイブ

その他