データに対し統一的なアクセス手段を提供してくれるLINQは便利な機能ではあるが、クセの強い特徴がある。
基本的にはそれらを意識しなくても効率的な形で実行されるのだが、場合によってはそれが裏目に出る時もある。
そうならないためにも、簡単にLINQの内部処理を理解することは重要である。
以下にLINQの3つの特徴を説明していく。
式を解析して効率化してくれる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static void Main(string[] args) { IEnumerable<int> list = Enumerable.Range(1, 20); //1~20までのコレクションを生成 Console.WriteLine("■偶数を抽出"); var evens = list.Where(l => IsEven(l)); Console.WriteLine("■4の倍数を抽出"); int mul4 = evens.Where(l => IsMultipleOf4(l)).Sum(); Console.WriteLine(mul4); ReadKey(); } //偶数か判定 public static bool IsEven(int num) { Console.WriteLine($"IsEven()実行, 引数{num}"); return num % 2 == 0; } //4の倍数か判定 public static bool IsMultipleOf4(int num) { Console.WriteLine($"IsMultipleOf4実行, 引数{num}"); return num % 4 == 0; } |
【出力結果】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
■偶数を抽出 ■4の倍数を抽出 IsEven()実行, 引数1 IsEven()実行, 引数2 IsMultipleOf4実行, 引数2 IsEven()実行, 引数3 IsEven()実行, 引数4 IsMultipleOf4実行, 引数4 IsEven()実行, 引数5 IsEven()実行, 引数6 IsMultipleOf4実行, 引数6 IsEven()実行, 引数7 IsEven()実行, 引数8 IsMultipleOf4実行, 引数8 IsEven()実行, 引数9 IsEven()実行, 引数10 IsMultipleOf4実行, 引数10 IsEven()実行, 引数11 IsEven()実行, 引数12 (省略) 60 |
コード上は、IsEven()のループ(※1)を回してから、IsMultipleOf4のループ(※2)を回している。
しかし、出力を見ると、ループが1回にまとまっていることがわかる。(1回のループで※1と※2が同時に実行されている。)
このようにLINQは式を解析して、最適な形で実行をしてくれる。
ただし、最適化が適用されるのは一つのメソッドチェーンで表せる範囲のみ。
例えば、上記のケースだと
1 |
int mul4 = list.Where(l => IsEven(l)).Where(l => IsMultipleOf4(l)).Sum(); |
という風に一つのメソッドチェーンで表すことができる。
しかし、以下のようなケースでは、IsEven()のループが2回まわってしまい、非効率な形で実施されてしまう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static void Main(string[] args) { IEnumerable<int> list = Enumerable.Range(1, 20); //1~20までのコレクションを生成 Console.WriteLine("■偶数を抽出"); var evens = list.Where(l => IsEven(l)); Console.WriteLine("■4の倍数を抽出"); int mul4 = evens.Where(l => IsMultipleOf4(l)).Sum(); Console.WriteLine(mul4); Console.WriteLine("■4の倍数でないものを抽出"); int mulNot4 = evens.Where(l => !IsMultipleOf4(l)).Sum(); Console.WriteLine(mulNot4); ReadKey(); } |
【出力結果】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
■偶数を抽出 ■4の倍数を抽出 IsEven()実行, 引数1 IsEven()実行, 引数2 IsMultipleOf4実行, 引数2 IsEven()実行, 引数3 IsEven()実行, 引数4 IsMultipleOf4実行, 引数4 IsEven()実行, 引数5 IsEven()実行, 引数6 IsMultipleOf4実行, 引数6 IsEven()実行, 引数7 IsEven()実行, 引数8 IsMultipleOf4実行, 引数8 IsEven()実行, 引数9 IsEven()実行, 引数10 IsMultipleOf4実行, 引数10 IsEven()実行, 引数11 IsEven()実行, 引数12 IsMultipleOf4実行, 引数12 IsEven()実行, 引数13 IsEven()実行, 引数14 IsMultipleOf4実行, 引数14 IsEven()実行, 引数15 IsEven()実行, 引数16 IsMultipleOf4実行, 引数16 IsEven()実行, 引数17 IsEven()実行, 引数18 IsMultipleOf4実行, 引数18 IsEven()実行, 引数19 IsEven()実行, 引数20 IsMultipleOf4実行, 引数20 60 ■4の倍数でないものを抽出 IsEven()実行, 引数1 IsEven()実行, 引数2 IsMultipleOf4実行, 引数2 IsEven()実行, 引数3 IsEven()実行, 引数4 IsMultipleOf4実行, 引数4 IsEven()実行, 引数5 IsEven()実行, 引数6 IsMultipleOf4実行, 引数6 IsEven()実行, 引数7 IsEven()実行, 引数8 IsMultipleOf4実行, 引数8 IsEven()実行, 引数9 IsEven()実行, 引数10 IsMultipleOf4実行, 引数10 IsEven()実行, 引数11 IsEven()実行, 引数12 IsMultipleOf4実行, 引数12 IsEven()実行, 引数13 IsEven()実行, 引数14 IsMultipleOf4実行, 引数14 IsEven()実行, 引数15 IsEven()実行, 引数16 IsMultipleOf4実行, 引数16 IsEven()実行, 引数17 IsEven()実行, 引数18 IsMultipleOf4実行, 引数18 IsEven()実行, 引数19 IsEven()実行, 引数20 IsMultipleOf4実行, 引数20 50 |
LINQは基本的には最適な形で実行されるが、場合によっては非効率な形式になってしまうこともある。
LINQが扱う式は実体がない
LINQが扱う式は実体をもたない。
コレクションに対し、どういう命令をするかという情報だけを返す。
メモリ上に新しいコレクションを生成するわけではない。
この性質のために、上記では非効率な形で式が実行されてしまっている。
(var evensはあくまで、listに対しIsEven()を実施するという情報のみ。
実際にint mul4などで必要になった時に、式が実行される。)
上のケースでIsEven()のループを先に実施してしまいたい場合は、以下のようにコレクションを実体化すればよい。
1 |
var evens = list.Where(l => IsEven(l)).ToList(); |
【出力結果】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
■偶数を抽出 IsEven()実行, 引数1 IsEven()実行, 引数2 IsEven()実行, 引数3 IsEven()実行, 引数4 IsEven()実行, 引数5 IsEven()実行, 引数6 IsEven()実行, 引数7 IsEven()実行, 引数8 IsEven()実行, 引数9 IsEven()実行, 引数10 IsEven()実行, 引数11 IsEven()実行, 引数12 IsEven()実行, 引数13 IsEven()実行, 引数14 IsEven()実行, 引数15 IsEven()実行, 引数16 IsEven()実行, 引数17 IsEven()実行, 引数18 IsEven()実行, 引数19 IsEven()実行, 引数20 ■4の倍数を抽出 IsMultipleOf4実行, 引数2 IsMultipleOf4実行, 引数4 IsMultipleOf4実行, 引数6 IsMultipleOf4実行, 引数8 IsMultipleOf4実行, 引数10 IsMultipleOf4実行, 引数12 IsMultipleOf4実行, 引数14 IsMultipleOf4実行, 引数16 IsMultipleOf4実行, 引数18 IsMultipleOf4実行, 引数20 60 ■4の倍数でないものを抽出 IsMultipleOf4実行, 引数2 IsMultipleOf4実行, 引数4 IsMultipleOf4実行, 引数6 IsMultipleOf4実行, 引数8 IsMultipleOf4実行, 引数10 IsMultipleOf4実行, 引数12 IsMultipleOf4実行, 引数14 IsMultipleOf4実行, 引数16 IsMultipleOf4実行, 引数18 IsMultipleOf4実行, 引数20 50 |
結果が必要になったら計算される(遅延実行)
先述したように、LINQの式は実体を持たず、実際にその値が使用される時にならないと計算は実行されない。
この実際にその値が使用される時というのは、より厳密にいうと「値が必要になってMoveNext()メソッドが実行された時」である。
例えば、以下のコードで考えてみる。
1 2 3 4 5 6 7 8 |
IEnumerable<int> list = Enumerable.Range(1, 20); //1~20までのコレクションを生成 Console.WriteLine("■偶数を抽出"); var evens = list.Where(l => IsEven(l)); Console.WriteLine("■4の倍数を抽出"); int mul4 = evens.Where(l => IsMultipleOf4(l)).Sum(); Console.WriteLine(mul4); |
これは、メソッドチェーンによって最適化されるので、以下のようになる。
1 |
int mul4 = list.Where(l => IsEven(l)).Where(l => IsMultipleOf4(l)).Sum(); |
さらに、LINQの実体はforeach文なので、以下のように書き換えられる。
1 2 3 4 5 |
int mul4 = 0; foreach (var l in list) { if (IsEven(l) && IsMultipleOf4(l)) mul4 += l; } |
さらにforeach文はコンパイル後は以下のような文に解析される。(参考)
1 2 3 4 5 6 7 |
int mul4 = 0; var enu = list.GetEnumerator(); while (enu.MoveNext()) //※1 { if (IsEven(enu.Current) && IsMultipleOf4(enu.Current)) mul4 += enu.Current; } Console.WriteLine(mul4); |
つまり、上記の※1のMoveNext()が実行される時に、はじめて計算が実行されることになる。