return IEnumerableとyield return
- 2009-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件のみ取得とか、それなりに自由に動かせます。