(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

(:3[kanのメモ帳]



LINQの遅延実行&即時実行とforeach+遅延実行の問題【C#】【LINQ】


このエントリーをはてなブックマークに追加




はじめに

今回はLINQの遅延実行と即時実行についての話。

LINQを使う上で知らなくてもわりとどうにかなる場合が多いですが、かなり大切な概念です。


ちなみに、「実戦で役立つ C#プログラミングのイディオム/定石&パターン」にも同様の項目があり、


実戦で役立つ C#プログラミングのイディオム/定石&パターン

実戦で役立つ C#プログラミングのイディオム/定石&パターン


かつ、これに関するちょっとした問題に遭遇したので記事にしてみました!


遅延実行

いきなりですが、以下の出力は何になると思いますか?

(ちなみにDebug.LogはUnityでのログ出力メソッド)

var nums = new List<int>(){1, 2, 5, 6, 10, 11, 20, 21};

var newNums = nums.Where(num => num > 10);

Debug.Log("--------出力1");
foreach (var num in newNums) {
  Debug.Log(num);
}

nums[0] = 100;
   
Debug.Log("--------出力2");
foreach (var num in newNums) {
  Debug.Log(num);
}


普通に考えたら、10より大きい数字を取り出した後に元の配列の中身を変えているので、

出力1と2は同じになりそうですが、実際は異なります。

--------出力1
11
20
21
--------出力2
100
11
20
21


これはなぜかというと、Whereは呼び出された時点で処理をして、その結果を返しているわけではなく、

どういう処理をするかの命令(クエリ)を返してるからなのです。

つまり、実際に値が必要になった時に処理を実行(遅延実行)しているのです。


もちろんLINQの中にも遅延実行ではないものもあります。

例えば最大値を返すMaxや要素数を返すCountなど、単一の値を返すメソッドはそのまま実行されます。

var nums = new List<int>(){1, 2, 5, 6, 10, 11, 20, 21};

var max = nums.Max();

Debug.Log("--------出力1");
Debug.Log(max);

nums[0] = 100;
   
Debug.Log("--------出力2");
Debug.Log(max);
--------出力1
21
--------出力2
21


ちなみに、遅延実行を含めて考えると、最初のコードのnewNumsという名前もおかしい事になります。

//var newNums = nums.Where(num => num > 10); //新しい配列を作ってるのではなく

//クエリを作っている
var query = nums.Where(num => num > 10);



foreach+遅延実行の問題

「配列の内容が変更される可能性がある時に、何度も同じ命令を実行したい」

みたいな場合には遅延実行は便利ですが、時には注意しなければならない場合もあります。


例えば「配列の要素になんらかの処理をして、場合によっては配列から要素を削除したい」

みたいな時に単純に配列をforeachで回してしまうと、

var nums = new List<int>(){1, 2, 5, 6, 10, 11, 20, 21};

foreach (var num in nums) {
      
  //0より大きければなんらかの処理(これがないならWhereだけでいい)
  if (num > 0) {
    /*なんらかの処理*/
  }
    
  //10より大きい要素は配列から削除
  if (num > 10) {
    nums.Remove(num);
  }

}


foreach内で元となってる配列を操作するなとエラーが出ます。

InvalidOperationException: Collection was modified; enumeration operation may not execute.


なので、元の配列ではなく抽出した配列を回そうと、Whereを使ってみますが、

var nums = new List<int>(){1, 2, 5, 6, 10, 11, 20, 21};

//元の配列でなく、0より大きい配列でforeach(したつもり)
foreach (var num in nums.Where(num => num > 0)) {
  /*なんらかの処理*/
    
  //10より大きい要素は配列から削除
  if (num > 10) {
    nums.Remove(num);
  }
}


残念ながらこれも先程と同様のエラーが出ます。

なぜなら、前項の通りWhereは配列を作ってるわけではなくクエリを作っているので、

別の配列をforeachで回してる事にはならないからです。


即時実行

先程の例のように場合によっては遅延実行ではなく、すぐに実行したい時もあります。

そんな時は最後にToListやToArrayを使うことで結果を確定(即時実行)させる事が出来ます。

var nums = new List<int>(){1, 2, 5, 6, 10, 11, 20, 21};

//元の配列でなく、0より大きい配列でforeach
foreach (var num in nums.Where(num => num > 0).ToList()) {
  /*なんらかの処理*/
    
  ///10より大きい要素は配列から削除
  if (num > 10) {
    nums.Remove(num);
  }
}


これならエラーが出ずに問題なく実行出来ます。


ちなみに最初の例でも、ToListで確定させると、出力1と2が同じになります。

var nums = new List<int>(){1, 2, 5, 6, 10, 11, 20, 21};

//ToListを使う事で、クエリではなく新しいListを取得
var newNums = nums.Where(num => num > 10).ToList();

Debug.Log("--------出力1");
foreach (var num in newNums) {
  Debug.Log(num);
}

nums[0] = 100;
    
Debug.Log("--------出力2");
foreach (var num in newNums) {
  Debug.Log(num);
}
--------出力1
11
20
21
--------出力2
11
20
21