前回の投稿拡張メソッドを使って自力で、Map を実装するは、今回のイテレーターブロックを理解するための、ステップなのです(自分にとって。)
早速イテレーターブロックに関して理解していきましょう。
イテレーターブロックとは
イテレータブロックとは、yield return
や yield break
を含むブロックのことです。特に前回紹介したような、イテレーターをゴネゴネしたいときにとても有効な書き方です。
前回のおさらいをしてみましょう。オレオレ Map をこんな感じで実装しました。
static class EmployeeExtensions
{
public static IEnumerable<X> Map<T, X>(this IEnumerable<T> e, Func<T,X> f)
{
List<X> x = new List<X>();
foreach(var element in e)
{
x.Add(f(element));
}
return x;
}
}
これだけで、次のようなコードをかくと、IEnumerable を実装したクラスが拡張されます。
var list = new List<int> { 1, 2, 3, 4 };
var result = list.Map<int, int>(p => p + 1);
foreach (var e in result)
{
Console.WriteLine(e);
}
Console.ReadLine();
結果
2
3
4
5
Map を実装してカッケーという感じですが、さらにおしゃれにかけます。今までは、Map の中で、List を作ってそこに、関数適用した結果を渡して、戻り値を作成していましたが、イテレーターブロックを使うとこんなに簡潔にかけます。
public static IEnumerable<X> CoolMap<T, X>(this IEnumerable<T> e, Func<T,X> f)
{
foreach(var element in e)
{
yield return f(element);
}
}
おしゃれすぎる、、、つまり、yield return が呼ばれるたびに f(element)
が イテレーターによって返却される感じです。じゃあ、それをつかったコードを書いてみますが、せっかくなのでちょっと変えてみましょう。
var strings = new List<string> { "a", "b", "c" };
var upper = strings.CoolMap<string, string>(p => p.ToUpper());
foreach(var e in upper)
{
Console.WriteLine(e);
}
Console.ReadLine();
実行結果。ううむいい感じ。ジェネリクス様様。
A
B
C
でたらめな結果を返してみる。
さて、yield return
の理解を深めるために、自分で自らでたらめな値を返却してみましょう。
public static IEnumerable<int> Random(this IEnumerable<int> e)
{
yield return 8;
yield return 6;
yield return 10;
yield return 19;
yield break;
yield return 20;
}
最初のリストなど関係ない。といったコードになっています。実行しましょう。
var dummy = new List<int> { 1, 2, 3, 4 };
foreach (var e in dummy.Random())
{
Console.WriteLine(e);
}
Console.ReadLine();
予想通り、リストで定義された 1, 2, 3, 4
は無視され、yield return
で指定した値が一つづつ返却されています。そして、yield break;
が来た時点でイテレーターは終了します。ですので、20
はスルーされています。
8
6
10
19
遅延実行
さて、この仕組みを理解したかったのは、師匠が、Linq では遅延実行がつかわれているという話でそのサンプルを書いてくれたからです。師匠サンプル
ここで使われているテクニックが理解できなかったため2つのブログを書いて理解しようとしたわけです。今はりかいできます。もともと私が持っていた疑問は次のものです。
var result = new T();
properties.Select(p => { p.SetValue(result, Environment.GetEnvironmentVariable(p.Name));
e = Environment.GetEnvironmentVariable(p.Name);
Console.WriteLine("Debug!:" + e);
return p; });
return result;
こういうコードを書いたのですが、うまく動かなかったのです。関数を適用したかったわけではなく、foreach ではなく、Linq でワンライナーでループできないかなと思いました。これは、リフレクションで、取ってきたメソッド名と同名の環境変数の値をセットしようとしていました。Linq は遅延評価らしいです。
しかし、これはうまくいきません。この書き方だと、この関数が実行されることはありません。なぜなら、properties.Select
の戻り値が使われるときにはじめて、この関数が適用されるからです。この書き方だと、properties.Select
の戻り値が捨てられているので、この関数適用が有効になることはありません。
イテレーターブロックで書くと、遅延評価になるようなのでコメントを入れて違いを確かめてみましょう。
イテレータブロックを使っていないMap
と、イテレーターブロックを使った CoolMap
を使ってコードを書いてみました。
var somelist = new List<int> { 1, 2, 3 };
Console.WriteLine("Start! Map!");
var justnowresult = somelist.Map<int, int>(p => {
Console.WriteLine("Doing!"); // Doing! の位置で評価タイミングがわかる!
return p + 1;
});
Console.WriteLine("Ready Go!");
foreach (var i in justnowresult)
{
Console.WriteLine(i);
}
Console.WriteLine("Finish!");
Console.WriteLine("Start! CoolMap!");
var lazyresult = somelist.CoolMap<int, int>(p => {
Console.WriteLine("Doing!"); // Doing! の位置で評価タイミングがわかる!違いに注意
return p + 1;
});
Console.WriteLine("Ready Go!");
foreach (var i in lazyresult)
{
Console.WriteLine(i);
}
Console.WriteLine("Finish!");
Console.ReadLine();
実行結果を見てみましょう。Map
に渡された関数は、Map適用時に既に評価されているの対して、CoolMap
に渡された関数は、foreach
のタイミングで評価されているのがわかります。
Start! Map!
Doing!
Doing!
Doing!
Ready Go!
2
3
4
Finish!
Start! CoolMap!
Ready Go!
Doing!
2
Doing!
3
Doing!
4
Finish!
というわけでやっとすっきりしました。Linq の Select はこんなイメージに似ていて遅延評価なんですね。また一つ学びましたし、イテレータブロックも学んだので、ライブラリ作るときに役立ちそうです!