ゆうなんとかさんの雑記帳的な。

Twitterで踊ったり音ゲーしたりしてるあの名前がよくわからない人が書いてるらしいよ。

LINQのSelectとWhereは順番が大事

LINQはSelectとWhereについて(もしかしたら私が情弱なだけでほかの関数もあるかもしれませんが)、連続して書いていると最適化がかかります。これにより、Selectを連続で書いている場合はだいたいこのようになります。

// クエリ式ではSelectの連続はletの連続で表現される
var result = from i in Enumerable.Range(0, 10)
             let  j = i * 10
             let  k = j / 5
             let  l = k + 2 
             select l.ToString();
// 上の式はだいたいこうなると思っていい(完全に同じではない)
var result = List<string>();
foreach (var i in Enumerable.Range(0, 10))
{
    result.Add((((i * 10) / 5) + 2).ToString());
}

Whereを連続で書いている場合はだいたいこんな感じ。

// 見た目通りWhereの連続はクエリ式だとこうなる
var result = from i in Enumerable.Range(0, 1000)
             where i & 1 != 0
             where i < 500
             where i > 100
             select i;
// 上の式はだいたいこうなると思っていい(完全に同じでは(ry
var result = List<int>();
foreach (var i in Enumerable.Range(0, 10))
{
    if((i & 1 != 0) && (i < 500) && (i > 100)) result.Add(i);
}

要は関数合成をやるわけですね。こういう最適化ができるのも即評価せず計算を定義するというワンステップを踏んでいるこそできる芸当だと思います。

さらに、「選択して射影する」というのはよくあるパターンなので、これも最適化がかかります。要するに「WhereしてSelect」です。これは順番が大事で、SelectしてWhereすると最適化がかからずループが2回まわります。

// SelectしてWhereするとだいたいこうなる(完全に(ry
var x = List<int>();
foreach (var i in Enumerable.Range(0, 100))
{
    x.Add(i * 5);
}
var result = new List<int>();
foreach (var i in x)
{
    if (i % 10 == 0) result.Add(i);
}
// WhereしてSelectするとだいたいこうなる(完(ry
var result = new List<int>();
foreach(var i in Enumerable.Range(0, 100))
{
    if (i % 2 == 0) result.Add(i * 5);
}

今回はクエリ式をListとforeachによる追加に見立てましたが、たぶんListにAddで突っ込むというよりはyield returnのほうが感覚的には近いかもしれません。

具体的にどう最適化されるか知りたいときは
neue cc - LINQのWhereやSelect連打のパフォーマンス最適化について
とか
LINQの仕組み&遅延評価の正しい基礎知識 − @IT
とかを見るといいと思います。