読者です 読者をやめる 読者になる 読者になる

IEnumerator<T> を実装していれば必ず IDisposable である理由

.NET

書こうと思ってタイミングを逃していた話があったのを,『パイプラインパターンとリソース管理』を読んでいて思い出しました.ので,今度こそ書いてみます.



皆様,IEnumerator<T> の基底インターフェイスに IDisposable が紛れ込んでいるのをご存じでしょうか?

public interface IEnumerator<T> : IDisposable, IEnumerator

つまり,IEnumerator は IDisposable であるとは限りませんが,IEnumerator<T> は必ず IDisposable ということになります.
Microsoft は何故このような設計にしたのでしょうか?



Enumerator が IDisposable を実装しているという可能性について,C# の foreach ステートメントが想定しているのはまあ割と有名な話かと思います.
興味深いのは,逆に C# の foreach ステートメントの仕様上,列挙が途中で中止されたことを直ちに enumerator が知るためには IDisposable.Dispose メソッドを実装する他ないという点です.
IEnumerator<T> 使用時の状態遷移図を見てみましょう.

IEnumerator.Reset は半分死に設定状態なのでまあいいとして,ポイントは「列挙終了」のステートを通らずに「破棄完了」に遷移した場合でも,IDisposable.Dispose で通知を受けられるというところです.
ちなみに C# 2.0 では Iterator によって「列挙前」*1「列挙中」「破棄完了」の処理をひとつのメソッドのように定義することができます.「列挙前」と「破棄完了」が含まれていることがポイントで,Iterator の中身は「列挙中」ステートだけではないことをよく憶えておいてください.


このように .NET 2.0 の IEnumerator<T> 実装時は,必ず IDisposable.Dispose も実装することになるので,collection が IEnumerable<T> を実装する場合,C# コンパイラは以下のようなコードを生成すると定められています.

// foreach (ElementType element in collection) statement の展開

IEnumerator<T> enumerator = ((IEnumerable<T>)(collection)).GetEnumerator();
try {
    while (enumerator.MoveNext()) {
        ElementType element = (ElementType)enumerator.Current;
        statement;
    }
}
finally {
    enumerator.Dispose();
}

元々『C# Language Specification 1.2』の「8.8.4 foreach ステートメント」で定義されている『コレクションパターン』では Dispose の実装は optional なのですが,どうもこれを必須なものとして再定義したかったという思惑が見て取れるような気がします.
恐らく今後は,C# の foreach という一言語の仕様のみならず,クラスライブラリ全体の仕様としてこの挙動が求められるのではないでしょうか.



という話を書こうと思ったそもそもの発端は,「矢野勉のはてな日記」にて『Rubyのブロック構文のように、Javaで全行処理し終わったら勝手に閉じるイテレータを作る』を読んだときのことなので,かれこれ半年前ですか.
誰かが『Java の拡張 for 文と C# の foreach の違い』といったタイトルで書くかと思ったのですが,誰も書かなさそうなので書いてみました.
そういえばもうすぐ C++/CLI にも STL/CLR が入りますが,元々の C++ STL iterator のセマンティクスを考えると,やはり IEnumerator<T> のセマンティクスとは微妙に異なるものになりそうな気がしますね.

おまけ C# foreach の暗黒トリビア

C# Language Specification 1.2』の「8.8.4 foreach ステートメント」をよーく読むと,IEnumerable インターフェイスを実装していなくても foreach に放り込めるケースがあることが分かります.

public class MyStringEnumerator
{
    int _currentIndex = -1;
    readonly string[] _msgs;
    bool InRange(int index) { return 0 <= index && index < _msgs.Length; }
    public MyStringEnumerator(string[] strings)
    {
        _msgs = strings;
    }
    public string Current
    {
        get
        {
            if (!InRange(_currentIndex))
                throw new InvalidOperationException();
            return _msgs[_currentIndex];
        }
    }
    public bool MoveNext()
    {
        _currentIndex++;
        return InRange(_currentIndex);
    }
    public void Reset()
    {
        _currentIndex = -1;
    }
}
public class MyStringEnumerable
{
    readonly string[] _msgs;
    public MyStringEnumerable(string[] strings)
    {
        _msgs = strings;
    }
    public MyStringEnumerator GetEnumerator()
    {
        return new MyStringEnumerator(_msgs);
    }
}
class Program
{
    static void Main(string[] args)
    {
        foreach (string s in new MyStringEnumerable(args))
        {
            Console.WriteLine(s);
        }
    }
}

(InRange の判定間違っていたのを修正)
実は,IEnumerable インターフェイスを実装していなくても,GetEnumerator や MoveNext,Current といったメソッドとプロパティのシグネチャが条件を満たしていれば,C# では foreach が使用可能です.
.NET に Generics が無かった時代に,何とか boxing を回避したいという思いから導入された特殊なコード生成規則という感じですね.

*1:Iterator 構文自体には enumerator のコンストラクタに処理を流し込む記法が存在しないので,「列挙前」が記述できるという点については削除.パイプラインパターンで,別の Iterator を返す前に処理を挟み込むのは可能