[Memo] デストラクタが呼ばれるタイミングの検証 その3 ~IDisposableインターフェースの実装~

スポンサーリンク

前回の記事

[Memo] デストラクタが呼ばれるタイミングの検証 その1
[Memo] デストラクタが呼ばれるタイミングの検証 その2 ~GC.Collect~


前回までの検証で、デストラクタが呼ばれるタイミングはクラスを使い終わったタイミングではないことがわかりました。
そこで、デストラクタに頼らず明示的にリソースの解放を行うために、IDisposableインターフェースを実装するという方法があります。

今回は、このIDisposableインターフェースを実装してみます。
.NETのガベージコレクタは、マネージリソースを対象としています。
アンマネージリソースはガベージコレクタの対象ではありません。
では、マネージリソースは何かというと、基本的にはメモリです。
よってファイルはメモリではないため、明示的にクローズ処理を実行する必要があります。

クラスにIDisposableインターフェースを実装することで、Disposeメソッドがあるよ、ということを明示的に示すことができるほか、usingステートメントを使用した確実なオブジェクト解放を行うことができるようになります。
usingステートメントは、抜けるタイミングで自動でDisposeメソッドを呼びます。

前回までのサンプルに新たなクラス CsvWriter(CsvWrite.cs)を追加します。
コードは以下の通りです。

using System;
using System.IO;

class CsvWriter : IDisposable
{
    bool _isDisposed = false;   // 破棄完了確認フラグ
    StreamWriter _sw;           // ファイル書き込み用ストリーム
    int _kokugoTotal = 0;       // 国語の合計点保持用
    int _sansuTotal = 0;        // 算数の合計点保持用
    int _count = 0;             // データ件数保持用

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="path"></param>
    public CsvWriter(string path)
    {
        Console.WriteLine("コンストラクタの呼び出し");

        _sw = File.CreateText(path);

        // ヘッダーの書き込み
        WriteHeader();
    }

    /// <summary>
    /// デストラクタ
    /// </summary>
    ~CsvWriter()
    {
        Console.WriteLine("デストラクタの呼び出し");

        ReleaseObject();

        Console.ReadLine();
    }

    /// <summary>
    /// CSVファイルのヘッダー作成
    /// </summary>
    private void WriteHeader()
    {
        _sw.WriteLine("氏名,国語,算数");
    }

    /// <summary>
    /// 一人の国語と算数の点数を書き込む
    /// </summary>
    /// <param name="name">氏名</param>
    /// <param name="kokugo">国語の点数</param>
    /// <param name="sansu">算数の点数</param>
    public void WriteData(string name, int kokugo,
         int sansu)
    {
        _kokugoTotal += kokugo;
        _sansuTotal += sansu;
        _count++;

        _sw.WriteLine(string.Format("{0},{1},{2}", name, kokugo, sansu));
    }

    /// <summary>
    /// 平均点を書き込む
    /// </summary>
    private void WriteAverage()
    {            
        _sw.WriteLine("平均,{0},{1}", _kokugoTotal / _count, _sansuTotal / _count );
    }

    private void ReleaseObject()
    {
        // 平均点の書き込み
        WriteAverage();

        // オブジェクト(StreamWrite)の破棄
        _sw.Dispose();

        // 破棄完了とする
        _isDisposed = true;
    }


    /// <summary>
    /// オブジェクトの解放
    /// </summary>
    public void Dispose()
    {
        Console.WriteLine("Disposeの呼び出し");

        if (!_isDisposed)
        {
            ReleaseObject();
        }
    }
}

このクラスでは、各個人の国語と算数の点数をCSVファイルに出力するというサンプルで、インターフェース IDisposableを継承しています。
IDisposableを継承した場合は Disposeメソッドを持たせる必要があります。
Disposeメソッドでは、不要になったオブジェクトを破棄するコードを実装します。

コンストラクタでは、CSVファイルを作成し、ヘッダー(データのタイトル)の書き込みを行っています。
デストラクタでは、オブジェクトの解放を行うために準備した ReleaseObjectメソッドを呼び出しています。
Disposeメソッドでは、まだオブジェクトの破棄が行われていない場合に(_isDispose 変数が false の場合)に ReleaseObjectメソッドを呼び出しています。
そのほかのメソッドは、CSVファイルにデータを書き込む処理をしているものです(今回の検証には関係ないので説明を省略します)

次に Program.cs を開いて Mainメソッドを以下のように編集します。

static void Main(string[] args)
{
    Console.WriteLine("開始");

    CsvWriter csv = new CsvWriter(@"C:\Work\Test.txt");

    csv.WriteData("徳川家康", 80, 70);
    csv.WriteData("織田信長", 90, 80);
    csv.WriteData("豊臣秀吉", 85, 95);

    // Disposeメソッドでオブジェクトを破棄
    csv.Dispose();

    Console.WriteLine("終了");

    Console.ReadLine();
}

ここで大事なのは、CsvWriterを使い終えた後、明示的にDisposeメソッドを呼び出してリソースを解放していることです。

以下、実行結果です。
終了前に Disposeメソッドが呼び出されていることがわかります。
また、当然ですがデストラクタも呼び出されています。

実行結果1

そして、Disposeメソッドでリソースを解放したのにもかかわらず、デストラクタが呼び出されてしまうために ReleaseObjectメソッドは2回呼ばれることとなり、結果として例外エラーが発生してしまいます。

実行結果2
Disposeメソッドを実装しているんだから、デストラクタは必要ないんじゃないの?と思われるかもしれませんが Disposeメソッドが呼び出されなかった場合でもリソースが解放されるようにデストラクタも準備しておくべきです。
(必ず Disposeを呼ぶからデストラクタは不要という考え方もありだと思います)

「Disposeメソッドを呼び出したら、デストラクタは実行されたくない」ということをガベージコレクタに伝える仕組みがあります。
これには、GC.SuppressFinalizeメソッドを使用します。

そこで、Disposeメソッドを以下のように編集します。

public void Dispose()
{
    Console.WriteLine("Disposeの呼び出し");

    if (!_isDisposed)
    {
        ReleaseObject();

        // ファイナライザ(デストラクタ)の呼び出しが不要であることを
        // ガベージコレクタに伝える
        GC.SuppressFinalize(this);
    }
}

上記のようにすることで、Diposeメソッドが呼び出された場合には、デストラクタは呼び出されなくなります。

最後に using ステートメントは本当に自動でDisposeを呼ぶのか実験をしてみます。

Mainメソッドを以下のように編集します。

static void Main(string[] args)
{
    Console.WriteLine("開始");

    using (CsvWriter csv = new CsvWriter(@"C:\Work\Test.txt"))
    { 
        csv.WriteData("徳川家康", 80, 70);
        csv.WriteData("織田信長", 90, 80);
        csv.WriteData("豊臣秀吉", 85, 95);
    }
    
    Console.WriteLine("終了");

    Console.ReadLine();
}

実行結果は以下の通りで、自動で Disposeメソッドが呼ばれていることがわかります。
usingステートメントを使うことで、 Disposeメソッドの呼び出しを忘れることがなくなりました。

実行結果3

次回は、Dispose後のアクセスに対処する方法について検証します。

Please follow and like us:

コメント

タイトルとURLをコピーしました