はじめに
遅延実行とは
プログラムの処理をすぐには実行せず、結果が必要になるまで「後回し」にする仕組み
どういうタイミングで実行されているのか、イメージしづらかったため、記事に起こして整理していく
なぜ知っておくべか
メモリ効率の向上
大量のデータを扱う場合、すべてのデータを一度にメモリは読み込む必要がなくなり、必要な分だけ処理するため、メモリ使用量を抑えることができる
パフォーマンスの最適化
必要のない処理をスキップできる
例えば、最初の10件だけ必要な場合取得して、残りのデータは処理しないとか
遅延実行と即時実行
C#のLINQには今すぐ実行される「即時実行」と後から実行される「遅延実行」の2つのパターンが存在する
遅延実行
以下コードは遅延実行を使ったサンプルの実装
任意の配列で作られた数字がある
Where()を使って、偶数を取得して、それを表示させるというプログラム
Where()ではただ、「偶数を取り出す」という計画を作っただけで、実際の処理はforeachで使うときに実行される
var numbers = new int[] { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
// この時点ではまだ、フィルタリングは実行されていない
// フィルタリングが実行される
foreach (var number in evenNumbers)
{
Console.WriteLine(number);
}
即時実行
ToList()をつけることで、フィルタリングされたものがすぐに実行され、反映される
// 即時実行
var evenNumberList = numbers.Where(n => n % 2 == 0).ToList();
foreach (var number in evenNumberList)
{
Console.WriteLine(number);
}
遅延実行の仕組み
裏側ではC#の「イテレータ」という機能使われている
public static IEnumerable<int> GetEvenNumbers(int[] numbers)
{
foreach (var n in numbers)
{
if (n % 2 == 0)
yield return n;
}
}
このyield returnが遅延実行の鍵となる
foreachなどで値が要求されるたびに、次の条件に合う値を1つずつ返す
全ての値を一度に処理して配列を作るのではなく、必要なときに必要な分だけ処理する
実際に動きを確認出来る例
// 遅延実行の様子を視覚的に確認
var numbers = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine("クエリ作成");
var query = numbers.Where(n => {
Console.WriteLine($"Where: {n}を処理中");
return n % 2 == 0;
});
Console.WriteLine("foreach開始");
foreach (var item in query)
{
Console.WriteLine($"結果: {item}");
}
結果
クエリ作成
foreach開始
Where: 1を処理中
Where: 2を処理中
結果: 2
Where: 3を処理中
Where: 4を処理中
結果: 4
Where: 5を処理中
Whereの処理がforeachの中で行われており、遅延実行がされていることが確認できる
どういうケースで遅延実行を使うのか
- 大量データを扱う場合
- I/O操作(ファイルなど)の効率化
どういう点を気をつけるべき
副作用
後から何か書き換えたりすることがある処理については、想定外の結果になるので気をつける
// 危険な例
int counter = 0;
var query = numbers.Select(n => counter++); // counterは後で増加
counter = 100; // ここでcounterを変更
// 実行されるのはここ - counterは100から始まる!
foreach (var item in query)
{
Console.WriteLine(item); // 想定外の結果になる
}
まとめ
遅延実行は「今すぐやる」というよりも、「必要になった時必要な分だけやる」という考え方
何も考えずにLINQを使っていると、思わぬところで想定外のことが起きるかもしれない
遅延実行を使った方がいいケース以外の時は、ToList()などを使って即時実行してた方が安全だと思った