IEnumerableでハマル

C#2.0からyield return文が使えるようになったおかげで、IEnumerableを返すメソッドが記述しやすくなった。
2.0以前はIEnumerableインターフェースを実装するクラスを作って、そのインスタンスを返す必要があったのでめんどくさかった。(というか、そもそも書いたことがない)

IEnumerableを返すメソッドは、foreachで回す分にはリストや配列と変らんわけですよ、実際。

public static List<int> GetRange(int num) {
    List<int> list = new List<int>();
    for(int i=0; i<num; i++) list.Add(i);

    return list;
}

public static IEnumerable<int> GetRange2(int num) {
    for(int i=0; i<num; i++) yield return num;
}

static void Main() {
    // foreachで使う分にはかわらん。
    foreach(int i in GetRange(10)) Console.WriteLine(i);
    foreach(int i in GetRange(10)) Console.WriteLine(i);
}

そうなると人間楽をしたがるんで、今までリストや配列で返していたメソッドをIEnumerableに変えていったりするわけですよ、
その本来の意味も考えずにね。

それで今日独自のコレクションクラスを書いていた時の話ですが。
えっ?ジェネリックがあるのに、いまさら何故独自のコレクションクラスを作るのかって?
たしかに、ただ追加削除するだけのコレクションクラスならListでいいわけですが、AddやRemoveメソッドの前や後に処理を挿入したい時にListではそれができないわけですよ、でもCollectionBaseCollectionならそれができる。だから作る。

まず、こんなクラスがある。

class Hoge {
    private string name;
    public string Name {
        get { return name; }
        set { name = value; }
    }
    public Hoge() { }
    public Hoge(string name) {
        this.name = name;
    }
}

そしてこれのコレクションクラス。

class HogeCollection : Collection<Hoge> {
    public void AddRange(Hoge[] hoges) {
        foreach(Hoge o in hoges) Add(o);
    }
}

そして、こんなショートカット的なメソッドも追加してみたりするわけです。

public Hoge Add(string name) {
    Hoge o = new Hoge(name);
    Add(o);

    return o;
}

そうなるとこれのAddRange版も欲しくなり作るわけですが、普通なら戻り値としてHogeの配列か、Listでも返すところを調子に乗ってIEnumerableとかやってしまったわけです。

public IEnumerable<Hoge> AddRange(string[] names) {
    foreach(string name in names) yield return Add(name);
}

で、こんな感じで使っちゃった。

public static void Main() {
    HogeCollection collection = new HogeCollection();
    collection.AddRange(new string[] { "hoge", "hogehoge" });
    // 追加されてるっしょ!
    Debug.Assert(collection.Count == 2);
}

これの結果はもちろんAssertに失敗してエラーですよ。
列挙子の意味をわかっていたはずなのに、あまりにも配列とかと同じ感覚で使えるから勘違いしてしまった。

yield return文で実際に返ってくるのは、C#コンパイラが自動生成したIEnumerableとIEnumeratorを実装したプライベートなクラスのインスタンスであり、呼び出し側でそのインスタンスのCurrentプロパティとMoveNextで各要素を列挙していく。この列挙が行われて、初めて実際のメソッドの処理が始まるわけだ。

これはコンパイルしたアセンブリを逆アセンブルしてILを見れば一目瞭然なので、疑うべくもない。

楽だからといって、適当に使っていたらこういうことになる。プログラムとはなんて奥が深いのだろうか、素晴らしい。