basyura's blog

あしたになったらほんきだす。

C# - LINQ でループの回り具合を再確認

LINQ 大好きなのはいいんだけど基本的に for で回してるのと同じだってことを認識していない人が多くて困る。魔法か何かと勘違されてる。これに起因してパフォーマンス悪化しまくり。LINQ 便利で使うのだけど、LINQ がない時にそんなにループで回すこと多かったかな?という疑問。隠蔽されることで、”とりあえず List” にしといて LINQ で取ればいいやで実装する人がやけに多い。その都度注意するのだけど分かったのか分かってないのか減らない。メソッドで引数を受けるときも IEnumerable で受ければいいのに List をにしてるからわざわざ ToList が必要だったり、必要ないのに ToList したりなかなかのカオス。

実際どういう動きになるのかを改めて確認してみる。

Where → Select したものを Any と foreach で呼び出す

Any or foreach を使うたびに LINQ が評価される。 Where を評価するための判断材料の生成コストや、Select で変換するためのコストが高いとその分パフォーマンスが悪化することになる。

コード

var list = new List<string>(){ "0", "1", "2", "3", "4" };

var targets = list.Where(v => {
    Console.WriteLine("Where  => " + v);
    return true;
}).Select(v => {
    Console.WriteLine("Select => " + v + "\n");
    return v;
});

Console.WriteLine("----- Any -----");
targets.Any();

Console.WriteLine("----- foreach -----");
foreach (var target in targets) {
}

出力

----- Any -----
Where  => 0
Select => 0

----- foreach -----
Where  => 0
Select => 0

Where  => 1
Select => 1

Where  => 2
Select => 2

Where  => 3
Select => 3

Where  => 4
Select => 4

foreach で止めてからの foreach

当たり前だけど?頭から再度回る。

コード

var list = new List<string>(){ "0", "1", "2", "3", "4" };

var targets = list.Where(v => {
    Console.WriteLine("Where  => " + v);
    return true;
}).Select(v => {
    Console.WriteLine("Select => " + v + "\n");
    return v;
});

Console.WriteLine("----- foreach 1 -----");
foreach (var target in targets.Select((v, i) => new { v, i }))
{
    if (target.i == 3)
    {
        Console.WriteLine("Break");
        break;
    }
}

Console.WriteLine("----- foreach 2 -----");
foreach (var target in targets.Select((v, i) => new { v, i }))
{
}

出力

----- foreach 1 -----
Where  => 0
Select => 0

Where  => 1
Select => 1

Where  => 2
Select => 2

Where  => 3
Select => 3

Break
----- foreach 2 -----
Where  => 0
Select => 0

Where  => 1
Select => 1

Where  => 2
Select => 2

Where  => 3
Select => 3

Where  => 4
Select => 4

List の ToList()

自身を返すと思いきや別インスタンスの List を返している。安全側に倒してるんだろう。

Reference Source

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    return new List<TSource>(source);
}

まとめ

現実問題としては LINQ による遅延評価のメリットを出す機会よりも、抽出した内容に対して複数の絞り込みをするケースが多いために ToList をせざるをえない機会が多い。そうなってしまうのはそもそものデータ構造やロジックの問題なので根が深そう。

試した環境は mac だけど、 brew install mono で入るし vim で quickrun 使えば即実行できるし便利。