return IEnumerableとyield return
- C# - 09.10/08
static void Main(string[] args) { var path = @"C:\test.txt"; var lines = EnumerateAllLines(path).ToArray(); } static IEnumerable<string> EnumerateAllLines(string filePath) { using (var sr = new StreamReader(filePath)) { return Enumerable.Repeat(sr, int.MaxValue) .TakeWhile(s => !s.EndOfStream) .Select(s => s.ReadLine()); } }
これの実行結果はどうなるでしょうか。答えは、「閉じているTextReaderから読み取ることはできません。」という例外が発生します。当たり前ですか? すみません。Linqばかり触っていると、ついついIEnumerableだから遅延評価だね!と単刀直入に思ってしまっていたりしたのですが、IEnumerableは決して必ずしも遅延評価であるということでは、ない。配列だってIEnumerableなんだよ!という。当然のようなことですが、すっかり頭から抜け落ちていました。反省。
何で例外が発生するかと言えば、EnumerateAllLines(path)の時点でメソッドが呼ばれ、returnで返した時点でusingを抜けてストリームが閉じられてしまう。ので、ToArray()で閉じられたストリームに対して読み込みを始めて、南無。というわけです。ではどうすればいいかというと……
static IEnumerable<string> EnumerateAllLines(string filePath) { using (var streamReader = new StreamReader(filePath)) { var seq = Enumerable.Repeat(streamReader, int.MaxValue) .TakeWhile(sr => !sr.EndOfStream) .Select(sr => sr.ReadLine()); foreach (var item in seq) yield return item; } }
yield returnを使ってやれば、コンパイラがイテレータを作るので、遅延実行される。EnumerateAllLines(path)の時点ではメソッド内部は一切通らない。MoveNextが呼ばれて初めてusingを通り、列挙が終わるかDisposeが呼ばれるまではusingを抜けない。という、なって欲しいであろう挙動を取ってくれるわけです。実行ファイルをReflectorで見ると、復元不可能なぐらいグチャグチャなものが出力されていて、あまりの難読化っぷりにビビりますが気にしないことにしませう。
そもそもEnumerable.Repeat(sr, int.MaxValue)のほうを改善してRepeatWithUsing作った方がいい、のではあるのですけど、まあ、それはそれということで。
無限リピート + SQL
上のはただの説明用の例でクソの役にもたたないので、もう少し実用的なものを一つ。
static void Main(string[] args) { var command = new SqlCommand(); command.CommandText = @"select hogehogehoge"; var result = command.EnumerateAll(dr => new { AA = dr.GetString(0), BB = dr.GetInt32(1) }); } static IEnumerable<T> EnumerateAll<T>(this IDbCommand command, Func<IDataReader, T> selector) { using (var reader = command.ExecuteReader()) { var seq = Enumerable.Repeat(reader, int.MaxValue) .TakeWhile(dr => dr.Read()) .Select(selector); foreach (var item in seq) yield return item; } } static T[] ReadAll<T>(this IDbCommand command, Func<IDataReader, T> selector) { return command.EnumerateAll(selector).ToArray(); }
シーケンス無限リピートをSQLの読み込みに応用してみるとかどうでしょう。Linq to Sqlのように、とまでは到底行きませんが、匿名型も使えるし、何となくそれっぽい雰囲気は出てるんじゃないかしらん。EnumerateAllの後段にTakeWhileを付けて条件で途中で止めるとか、Take(10)で10件のみ取得とか、それなりに自由に動かせます。
