2018年4月7日土曜日

アンマネージドリソースをDisposeパターンで管理する

モダンな言語や環境はガベージコレクタ(GC)が付いているのが当たり前になった時代です。GCによってどこからも参照されなくなったインスタンスは適当なタイミングで解放されるため、「メモリを解放する」という本来ならばものすっごく神経を使う作業から人類は解放されました。それはそれで便利で歓迎されることなのですが、かと言ってありとあらゆるリソースの解放をGC任せにできるわけでもありません。

そこで、.NET FrameworkにはIDisposableというインターフェイスを用意されていて、もうこれ以上このインスタンスは使わないから明示的にリソースを解放したいと思ったときにはDisposeメソッドを呼べばいいようになっています。
このインターフェイスは様々なクラスで使用されており、例えばイベントの購読停止だったり、ファイルのロックの解除だったりといった終了処理が行われます。また、言語面でも優遇されており、using構文を使うことで自動的にtry-finally構文に展開され確実にDisposeメソッドを呼ぶことができるようにもなります。

逆に、自動で解放されないリソースを使用する場合、それを使うクラスでIDisposableインターフェイスを実装しなければなりません。
IDisposeインターフェイスがDisposeメソッド1つのみしか持たないので「そのDisposeメソッドの中で解放処理をすればいいんでしょ?」と思ってしまいたくなりますが、実は話はそんなに単純ではありません。Disposeパターンと呼ばれるもうちょっとしっかりした実装が必要になってくるので、この記事では自動で解放されないリソースをサンプルとして使いつつその実装のしかたを見ていきたいと思います。

Disposeパターン

さて、 Disposeパターンはどういう書き方をするのか、ひな形を暗記しなければならないのかと思ったそこのあなた、心配いりません。IntelliSenseがひな形を用意してくれています。
これに従ってひな形を作るとこのようなコードが自動生成されます。
public class ExcelApp : IDisposable
{
    #region IDisposable Support
    private bool disposedValue = false; // 重複する呼び出しを検出するには

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue) {
            if(disposing) {
                // TODO: マネージ状態を破棄します (マネージ オブジェクト)。
            }

            // TODO: アンマネージ リソース (アンマネージ オブジェクト) を解放し、下のファイナライザーをオーバーライドします。
            // TODO: 大きなフィールドを null に設定します。

            disposedValue = true;
        }
    }

    // TODO: 上の Dispose(bool disposing) にアンマネージ リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします。
    // ~ExcelApp() {
    //   // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
    //   Dispose(false);
    // }

    // このコードは、破棄可能なパターンを正しく実装できるように追加されました。
    public void Dispose()
    {
        // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
        Dispose(true);
        // TODO: 上のファイナライザーがオーバーライドされる場合は、次の行のコメントを解除してください。
        // GC.SuppressFinalize(this);
    }
    #endregion
}
コメントの翻訳がガバガバで多少見苦しいですが、目を瞑ってあげましょう。

まず注目すべきは、インターフェースで規定されたDispose()メソッド以外にDispose(bool disposing)メソッドが用意されています。Dispose()メソッドからはDispose(bool disposing)メソッドを呼び出しているだけになっていますね。

実は、リソース解放処理を行うのはDispose(bool disposing)メソッドのほうになっています。この引数のdisposingは「Dispose()メソッドの呼び出しによってリソース解放処理を行うのかどうか」を示す引数です。

GCがこのクラスを回収に来た時、すなわちファイナライザが呼ばれたときは、そのクラス内で使っているGCが回収できるリソース(マネージドリソース)を敢えて解放する必要はありません。なぜならば、それらもGCが回収するからです。ですが、わざわざDispose()メソッドを呼び出してリソースを解放しようとするときは、内部的に使っているマネージドリソースもしっかり解放してあげないと、当然そのタイミングではGCが回収しに来ないためリソース解放漏れになります。

逆に、アンマネージドリソースは、Dispose()が呼び出されたときでもファイナライザが呼び出されたときでも確実に解放する必要があります。ですので、if(disposing)のブロックの外に解放処理を書き、ファイナライザをコメントアウト解除する必要があります。
また、ファイナライザに呼ばれるより先にDispose()メソッドが呼ばれた場合は、解放処理が重複してしまいますので、GC.SuppressFinalize(this);を呼び出してファイナライザの作動を抑制する必要があります。

あとは、disposedValueフィールドですでにこのクラスが破棄されたかどうかを保持しておりますので、もしもDispose後にこのクラスのメソッドやプロパティにアクセスされたときはObjectDisposedExceptionを投げるようにしてあげましょう。

サンプルコード:COMによるC#からのExcel操作

さて、Disposeパターンの実装がわかったところで、自動で解放されないリソースをDisposeパターンで記述するサンプルコードを書いてみましょう。
自動で解放されないリソースの代表格としてCOMがあります。「C# Excel」とかでググるといくらでもCOMを使った記事が出てきますが、その記事の数だけ「ソフトを終了したのにタスクマネージャーを開いたらEXCEL.EXEが残ったままだ」というコメントが見られることからも、リソースの解放が重要になってくるリソースです。
using Excel = Microsoft.Office.Interop.Excel;

public class ExcelApp : IDisposable
{
    Excel.Application excel = null;
    Excel.Workbook workbook = null;
    
    public ExcelApp(string filename)
    {
        excel = new Excel.Application();
        excel.Visible = true;

        workbook = excel.Workbooks.Open(filename);
    }

    #region IDisposable Support

    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue) {
            if(disposing) {
                // TODO: Managed Objectの破棄
            }

            if(workbook != null) {
                workbook.Close();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook);
                workbook = null;
            }

            if(excel != null) {
                excel.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excel);
                excel = null;
            }

            disposedValue = true;
        }
    }

    ~ExcelApp()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion
}
さて、Disposeパターンに従って書くとこのような感じになります。
COMはアンマネージドリソースですので、ファイナライザやSupposeFinalizeメソッドのコメントアウトを解除する必要があります。

ときどきSystem.Runtime.InteropServices.Marshal.ReleaseComObject(obj);の必要性やについての議論や、ひどい場合は「これを書かないからEXCEL.EXEプロセスが残ってしまう」といった誤った記述がされているサイトがありますが注意してください。
Workbook.Close()でワークブックを閉じて、Application.Quit()でアプリケーションを閉じさえすればEXCEL.EXEプロセスは正常に終了されます。Marshal.ReleaseComObjectは、.NETからのCOMオブジェクト(=Excelを操作するためのインターフェイス)を解放するためのメソッドで、これの有無にかかわらずExcelはしっかりと終了してくれます。
リソースの解放をしたらそれ以降はExcelの操作をしないわけですから、COMオブジェクトを解放しておくべきでしょう。

こうすることで
using(var excel = new ExcelApp(@"test.xlsx")) {

}
このように書くだけで、usingブロックを抜けたときにExcelがしっかりと終了されます。この場合はDispose()メソッドが呼ばれることによりExcelの終了処理が行われていることがわかります。

他にも、敢えてDispose()を呼び出さないようなコードを書いても、ちゃんとアプリケーション終了時にGCがファイナライザを呼び出してExcelが終了されることが分かります。 このDisposeパターンは確実にアンマネージドリソースを解放する方法として有効でしょう。

※上記のDisposeパターンのプログラムは、Disposeパターンの説明をするために書いているものでExcelを終了させるうえで完璧ではない点に注意してください。例えば、Excelを開いている間にファイルに変更を加え、保存せずにDisposeメソッドが呼ばれるとExcelのメッセージボックスで終了処理がブロックされます。
利用者は、各自アレンジして使ってください。

0 件のコメント:

コメントを投稿