[Memo] デストラクタが呼ばれるタイミングの検証 その4 ~Dispose処理後のオブジェクトへのアクセス~

前回の記事

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


タイトルは「デストラクタが呼ばれるタイミングの検証」となっていますが、内容は副題の方となります。

前回の検証で、Disposeによるリソースの解放について見ました。
今回は解放したリソースにアクセスされた場合の対処方法についてです。

前回の CsvWriterクラスを見ていただけるとわかるのですが、_isDisposedという変数を準備しています。
そして、ReleaseObjectメソッドの中で _isDisposed = true; とし、オブジェクトを解放したことをわかるようにしています。

解放されたリソースにアクセスされないようにするには、この変数をチェックすればOKです。
ということで、解放後にアクセスされたくない場所でチェックできるようにしておきます。
以下のようなコードを埋め込んでおくとよいでしょう。

if (_isDisposed) return;

また、解放されたオブジェクトにアクセスされた場合にスローする例外として ObjectDisposedException というものがあります。
先ほどのコードとともに使用する場合には、以下のように記述すればよいでしょう。

if (_isDisposed)
    throw new ObjectDisposedException(GetType().FullName);

以上で検証を終わりたいと思います。

[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後のアクセスに対処する方法について検証します。

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

前回の記事

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


前回の検証で、デストラクタが走るタイミングは、必ずしもクラスを使い終わったときと限らないことがわかりました。

そこで、こんどは GC.Collectメソッドを使用して強制的にガベージコレクションを実行し、デストラクタが呼ばれるかを検証してみました。

前回の Program.cs にある CreateSampleメソッドを以下のように書き換えます。
これで、GC.Collectによって強制的にガベージコレクションが実行されるので、デストラクタは「Sampleのインスタンス生成終了」の文字の前に実行されるはずです。

static void CreateSample()
{
    Console.WriteLine("Sampleのインスタンス生成");

    new Sample();

    GC.Collect();

    Console.WriteLine("Sampleのインスタンス生成終了");
}

実行結果は以下の通りです。

実行結果1
あれ?前回と変わらない….
確かに、ガベージコレクションは強制的に実行されるのですが、ガベージコレクションが実行されるタイミングと次に実行される命令との間で同期はとられないようです。

同期をとるには(というよりガベージコレクションが終了するのを待つには)、GC.WaitForPendingFinalizersメソッドを呼び出す必要があります。
そこで、 GC.Collectの実行後に GC.WaitForPendingFinalizersメソッドを呼び出すように書き換えてみます。

static void CreateSample()
{
    Console.WriteLine("Sampleのインスタンス生成");

    new Sample();

    GC.Collect();
    GC.WaitForPendingFinalizers();

    Console.WriteLine("Sampleのインスタンス生成終了");
}

実行結果は以下の通りです。
今度は、ガベージコレクションの実行が終わるのを待つので、デストラクタが呼び出された後に「Sampleのインスタンス生成終了。」が表示されています。

実行結果2

以上のように、GC.Collectを実行したからと言って必ずしもオブジェクトが破棄されてから次の命令が実行されるわけではありません。
また、前回も述べたとおり、クラスを使い終わったからと言って、即デストラクタが呼び出されるわけでもありません。
(この辺がガベージコレクションの仕組みで、オブジェクトが不要になったのを見計らって破棄を実施するので、そのタイミングはいつなのかはわかりません)

オブジェクトの破棄のタイミングは、 GC.Collectメソッドで決めることができますが、場合によっては GC.WaitForPendinGinalizersメソッドが必要だということを覚えておきましょう。
基本的に、ガベージコレクションは自動で実行されるため、このような場面に遭遇するのはあまり多くはないと思います。