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に変えていったりするわけですよ、
その本来の意味も考えずにね。
それで今日独自のコレクションクラスを書いていた時の話ですが。
えっ?ジェネリックがあるのに、いまさら何故独自のコレクションクラスを作るのかって?
たしかに、ただ追加削除するだけのコレクションクラスならListCollectionBaseCollection
まず、こんなクラスがある。
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
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を見れば一目瞭然なので、疑うべくもない。
楽だからといって、適当に使っていたらこういうことになる。プログラムとはなんて奥が深いのだろうか、素晴らしい。