Close() vs Dispose()

ここで Java の話を書いていたけど、.NET 界隈でも「SYSK 53: To Close() or to Dispose()? That is the question…」のような議論が昔からあったらしいということをさっき知った。GotDotNetの掲示板にも関連トピあり

つまりここで問題となっているのは、


  • メソッド名が開発者に想起させるセマンティクス


    例えば、Close() という関数名は、様々なアーキテクチャで、ファイルハンドルやファイルディスクリプタにおける操作では特に、ハンドルに関連づけられたオブジェクトの利用を終了することを意味することから、オブジェクトの破棄を連想させる。など。



  • オブジェクトのインスタンスとしての有効期間

    あるオブジェクトが意味を持った瞬間から、意味を失うときまで -- つまり System.Data.SqlClient.SqlConnection の場合なら Open() から Close() するまで




  • オブジェクトの背後にあるリソースのライフサイクル

    オブジェクトの意味合いを提供するリソースが背後にある場合



  • オブジェクトのライフサイクル

    オブジェクトを表現するメモリストリッジが確保されてから、メモリ上から消え去るまで -- つまりコンストラクタが呼ばれてから Finalize されるまで



がかみ合っていないという状況ですよね。

再利用可能なオブジェクト

SqlConnection の場合は、再利用可能であったという点でますます混乱が増長されてしまいました。リソースへのアクセス手段のみを提供するラッパオブジェクトでありがちな話です。

個人的には再利用が可能なオブジェクトはメモリリークやレースコンディションなど予期せぬバグを生むので、よほどパフォーマンスに繊細な状況でない限り避けるべきと思いますし、リサイクル可能な状況であっても、できるかぎり破棄してしまった方が健康的な気がします (つまり上の記事の主張のように)。

再利用可能かどうか、という性質の定義は実はややこしいと思います。

  1. ステートフルなオブジェクトの、役割上の状態とプロパティが、初期化された直後と同等の状態に戻る。
  2. ステートフルなオブジェクトの、役割上の状態が、初期化された直後の状態に戻る。
  3. ステートレスなオブジェクトの、プロパティが、初期化された直後と同等の状態に戻る。

このうちのどれを再利用可能性に期待するかで大きく実装も変わるからです。

「役割上の状態」とは苦し紛れに生み出した表現なので、もっとふさわしいことばがあれば置き換えたいです。要はオブジェクトの内部状態の遷移と、外から見えるオブジェクトの状態の遷移とは別だということを言いたいだけなのですが。

1, 2 が同居する状態もあります。長いサンプルコードですが我慢のほどを。

using System;
using System.Collections;
using System.Diagnostics;

interface IRecyclableParams {}

interface IRecyclable
{
    void Recycle(IRecyclableParams p);
}

// int配列をラップするIEnumerableオブジェクト
class IntArrayWrapper: IEnumerable
{
    int[] elts;
    int age;

    public IntArrayWrapper(int[] elts)
    {
        this.elts = elts;
        this.age = 0;
    }

    // 要素へのアクセッサ
    public int this[int idx]
    {
        get {
            return elts[idx];
        }

        set {
            elts[idx] = value;
            age++;
        }
    }

    // コレクションの変更検出用
    public int Age
    {
        get {
            return age;
        }
    }

    public int Length
    {
        get {
            return elts.Length;
        }
    }

    public IEnumerator GetEnumerator()
    {
        IEnumerator retval = new IntArrayEnumerator(
                new IntArrayEnumeratorParams(this));

        Trace.WriteLine("Instance " + retval.GetHashCode() + " created.");
        return retval;
    }
}

struct IntArrayEnumeratorParams: IRecyclableParams
{
    public IntArrayWrapper array;

    public IntArrayEnumeratorParams(IntArrayWrapper array)
    {
        this.array = array;
    }
}

// いろいろな意味でリサイクル可能な列挙子の実装
class IntArrayEnumerator: IRecyclable, IEnumerator
{
    IntArrayWrapper array;
    int idx;
    int age;
    int next_val;

    public IntArrayEnumerator(IntArrayEnumeratorParams p)
    {
        Recycle(p);
    }    

    public void Recycle(IRecyclableParams p)
    {
        idx = -1;
        array = ((IntArrayEnumeratorParams)p).array;
        age = array.Age;
    }

    public bool MoveNext()
    {
        if (age != array.Age)
            throw new InvalidOperationException("Collection has been changed");

        if (++idx < array.Length) {
            next_val = array[idx];
            return true;
        }

        return false;
    }

    public object Current
    {
        get {
            if (age != array.Age)
                throw new InvalidOperationException("Collection has been changed");
            if (idx < 0)
                throw new InvalidOperationException("Enumerator is not started");
            if (idx >= array.Length)
                throw new InvalidOperationException("Enumerator reached end of collection");
            return next_val;
        }
    }

    public void Reset()
    {
        if (age != array.Age)
            throw new InvalidOperationException("Collection has been changed");
        idx = -1;
    }
}

public class test
{
    public static void Main(string[] args)
    {
        IntArrayWrapper a = new IntArrayWrapper(new int[] { 1, 2, 3, 4, 5 } );

        Trace.Listeners.Add(new TextWriterTraceListener(Console.Error));

        Trace.WriteLine("* 普通に foreach で列挙");
        Trace.Indent();
        foreach (int i in a) {
            Console.WriteLine(i);
        }
        Trace.Unindent();

        Trace.WriteLine("* while で列挙");
        Trace.Indent();
        IEnumerator e = a.GetEnumerator();
        while (e.MoveNext()) {
            Console.WriteLine(e.Current);
        }
        Trace.Unindent();

        Trace.WriteLine("* 上で作ったインスタンスを IEnumerator 的に再利用");
        Trace.Indent();
        for (e.Reset(); e.MoveNext(); ) {
            Console.WriteLine(e.Current);
        }
        Trace.Unindent();

        Trace.WriteLine("* コレクションを変更する");
        a[0] = -1;
      
        Trace.WriteLine("* 上で作ったインスタンスをまた IEnumerator 的に再利用 (例外がでるはず)");
        Trace.Indent();
        try {
            for (e.Reset(); e.MoveNext(); ) {
                Console.WriteLine(e.Current);
            }
        } catch (Exception ex) {
            Trace.WriteLine(ex);
        }
        Trace.Unindent();

        Trace.WriteLine("リサイクルメソッドを呼んで IRecyclable 的に再利用");
        Trace.Indent();
        ((IRecyclable)e).Recycle(new IntArrayEnumeratorParams(a));
        while (e.MoveNext()) {
            Console.WriteLine(e.Current);
        }
        Trace.Unindent();
    }
}

これを /d:TRACE をつけてコンパイル、実行すると、

* 普通に foreach で列挙
    Instance -1321625920 created.
1
2
3
4
5
* while で列挙
    Instance 1737335424 created.
1
2
3
4
5
* 上で作ったインスタンスを IEnumerator 的に再利用
1
2
3
4
5
* コレクションを変更する
* 上で作ったインスタンスをまた IEnumerator 的に再利用 (例外がでるはず)
    System.InvalidOperationException: Collection has been changed
  at IntArrayEnumerator.Reset () [0x00000] 
  at test.Main (System.String[] args) [0x00000] 
リサイクルメソッドを呼んで IRecyclable 的に再利用
-1
2
3
4
5

という出力がされると思います (Mono上で実行)。

このサンプルでいう IEnumerator 的に再利用というのが 2. のケース、IRecyclable 的にリサイクルというのが 1. のケースに該当します。