C#の遅延実行とは?LINQで起こるタイミングと注意点をわかりやすく解説
はじめに
C#でLINQを使っていると、「Whereを書いたのに処理されていない」「foreachした瞬間に急に処理が走った」「ToListを付けたら結果が変わった」といった挙動に出会うことがあります。
この仕組みの中心にあるのが遅延実行です。
遅延実行は、C#のLINQを理解するうえで非常に重要な考え方です。便利な一方で、仕組みを知らずに使うと、同じ処理が何度も実行されたり、元データの変更によって結果が変わったり、思わぬタイミングで例外が発生したりします。
この記事では、C#の遅延実行とは何か、LINQで実行されるタイミング、注意点、メリット、対処法までを初心者にもわかりやすく解説します。
1. C#の遅延実行とは?
1-1. 遅延実行の意味を初心者向けに解説
C#の遅延実行とは、処理を定義した時点では実行せず、実際に結果が必要になったタイミングで処理を実行する仕組みです。
たとえば、LINQで次のようなコードを書いたとします。
C#var query = numbers.Where(n => n % 2 == 0);
このコードを見ると、numbersの中から偶数だけを取り出しているように見えます。しかし、多くの場合、この時点ではまだ偶数の抽出処理は実行されていません。
実際に処理が実行されるのは、次のようにforeachで列挙したり、ToList()でリスト化したりしたタイミングです。
C#foreach (var number in query)
{
Console.WriteLine(number);
}
つまり、遅延実行では「処理の予約」だけを先に作り、必要になったときに初めて実行します。
1-2. 「クエリを書いた時点」では処理されない仕組み
LINQのWhereやSelectなどは、結果そのものをすぐに作るのではなく、IEnumerable<T>のような「列挙できるオブジェクト」を返します。
このIEnumerable<T>は、データをすぐに持っているというよりも、「どうやってデータを取り出すか」という処理の流れを持っています。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(n =>
{
Console.WriteLine($"{n}を判定");
return n % 2 == 0;
});
Console.WriteLine("クエリ作成後");
このコードでは、Whereの中にConsole.WriteLineがありますが、queryを作っただけでは表示されません。
なぜなら、Whereはこの時点では実行されず、「列挙されたらこの条件で絞り込む」という処理を準備しているだけだからです。
1-3. 即時実行との違い
遅延実行と対になる考え方が即時実行です。
即時実行とは、クエリを書いた時点、またはメソッドを呼び出した時点で、その場で処理を実行して結果を確定することです。
代表的な即時実行の例はToList()です。
C#var list = numbers.Where(n => n % 2 == 0).ToList();
この場合、ToList()を呼び出した時点でWhereの条件判定が実行され、結果がList<int>として確定します。
遅延実行では、元データが後から変わると結果も変わる可能性があります。一方、即時実行では、その時点の結果をリストや配列として保存するため、元データが後で変わっても確定済みの結果は変わりません。
1-4. 遅延実行がLINQで重要になる理由
LINQでは、Where、Select、OrderBy、Takeなどを組み合わせてデータを処理します。
遅延実行を理解していないと、次のような問題が起こりやすくなります。
クエリを作っただけで処理されたと思ってしまう
同じクエリを複数回列挙して、同じ処理を何度も実行してしまう
元データの変更によって結果が変わることに気づかない
例外が発生するタイミングを誤解する
Entity FrameworkでSQLがいつ発行されるかを見誤る
C#の遅延実行は、LINQを効率よく使うための強力な仕組みですが、同時に注意点もあります。LINQの挙動を正しく理解するには、「いつ実行されるのか」を意識することが大切です。
2. LINQで遅延実行が起こるタイミング
2-1. WhereやSelectを書いただけでは実行されない
LINQのWhereやSelectは、代表的な遅延実行のメソッドです。
C#var query = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 10);
このコードを書いた時点では、まだWhereもSelectも実行されていません。
queryには、次のような処理の流れが保存されています。
numbersから要素を取り出す偶数かどうか判定する
偶数なら10倍に変換する
しかし、この処理はまだ実際には走っていません。結果を取り出そうとしたタイミングで初めて実行されます。
2-2. foreachで列挙したタイミングで実行される
遅延実行のクエリは、foreachなどで列挙したタイミングで実行されます。
C#foreach (var item in query)
{
Console.WriteLine(item);
}
このforeachが始まると、LINQは元データから1件ずつ要素を取り出し、WhereやSelectの処理を実行します。
重要なのは、すべての処理を先にまとめて実行するとは限らないことです。多くのLINQメソッドは、必要な要素を1つずつ処理します。
たとえば、Whereで条件に合う要素を探し、Selectで変換し、foreachの本体に渡す、という流れが要素ごとに進みます。
2-3. ToList・ToArray・Count・Firstで即時実行される
LINQには、呼び出した時点でクエリを実行するメソッドもあります。
代表的なものは次のとおりです。
C#var list = query.ToList();
var array = query.ToArray();
var count = query.Count();
var first = query.First();
ToList()やToArray()は、クエリの結果をすべて列挙してコレクションに変換します。
Count()は件数を数えるためにクエリを実行します。
First()は最初の要素を取得するためにクエリを実行します。ただし、First()は条件に合う最初の要素が見つかった時点で処理を終了できるため、必ずしも全件を処理するとは限りません。
2-4. 実行タイミングをコード例で確認する
次のコードで、遅延実行のタイミングを確認できます。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(n =>
{
Console.WriteLine($"{n}を判定中");
return n % 2 == 0;
});
Console.WriteLine("クエリを作成しました");
foreach (var number in query)
{
Console.WriteLine($"結果: {number}");
}
実行結果は次のようになります。
クエリを作成しました
1を判定中
2を判定中
結果: 2
3を判定中
4を判定中
結果: 4
5を判定中
Whereを書いた時点では判定処理は実行されていません。foreachで列挙したタイミングで、初めてWhereの中の処理が実行されています。
3. 遅延実行されるLINQメソッドと即時実行されるLINQメソッド
3-1. 遅延実行される代表的なメソッド
LINQで遅延実行される代表的なメソッドには、次のようなものがあります。
C#Where
Select
SelectMany
Take
Skip
OrderBy
OrderByDescending
ThenBy
ThenByDescending
GroupBy
Distinct
Concat
Union
Intersect
Except
これらのメソッドは、基本的にIEnumerable<T>やIQueryable<T>を返します。
たとえば、次のコードではWhereもSelectも遅延実行されます。
C#var query = users
.Where(u => u.Age >= 20)
.Select(u => u.Name);
この段階では、まだ成人ユーザーの抽出も名前の取得も行われていません。実際に列挙したタイミングで処理されます。
3-2. 即時実行される代表的なメソッド
即時実行される代表的なメソッドには、次のようなものがあります。
C#ToList
ToArray
ToDictionary
ToHashSet
Count
LongCount
Any
All
First
FirstOrDefault
Single
SingleOrDefault
Last
LastOrDefault
Max
Min
Sum
Average
Aggregate
これらは、呼び出したタイミングでクエリを実行し、具体的な値やコレクションを返します。
C#int count = users.Where(u => u.Age >= 20).Count();
この場合、Count()を呼び出した時点で、Whereの条件判定が実行されます。
3-3. ストリーミング処理と非ストリーミング処理の違い
LINQの遅延実行には、さらにストリーミング処理と非ストリーミング処理という違いがあります。
ストリーミング処理とは、要素を1つずつ処理しながら結果を返せる処理です。
C#var query = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 10);
WhereやSelectは、1つの要素を受け取り、条件判定や変換を行い、すぐ次に渡せます。そのため、全件を先に読み込む必要がありません。
一方、非ストリーミング処理は、結果を返す前に元データ全体を確認する必要があります。
C#var query = numbers.OrderBy(n => n);
OrderByは並び替えを行うため、すべての要素を見てからでないと最初の結果を決められません。そのため遅延実行ではありますが、列挙が始まった時点で内部的には全体を処理する必要があります。
つまり、遅延実行だからといって、必ず1件ずつ軽く処理されるとは限りません。
3-4. メソッドごとの実行タイミング早見表
| メソッド | 実行タイミング | 補足 |
|---|---|---|
Where | 遅延実行 | 条件に合う要素を抽出する |
Select | 遅延実行 | 要素を変換する |
Take | 遅延実行 | 指定件数だけ取得する |
Skip | 遅延実行 | 指定件数を飛ばす |
OrderBy | 遅延実行 | 列挙時に全体の並び替えが必要 |
GroupBy | 遅延実行 | 列挙時にグループ化される |
Distinct | 遅延実行 | 重複を除外する |
ToList | 即時実行 | 結果をリストに確定する |
ToArray | 即時実行 | 結果を配列に確定する |
Count | 即時実行 | 件数を返す |
Any | 即時実行 | 条件に合う要素の有無を返す |
First | 即時実行 | 最初の要素を返す |
Sum | 即時実行 | 合計値を返す |
Average | 即時実行 | 平均値を返す |
4. C#の遅延実行で起こりやすい注意点
4-1. 元データを変更すると結果も変わる
遅延実行では、クエリを作成した時点では結果が確定していません。そのため、元データを後から変更すると、実行結果も変わることがあります。
C#var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n >= 2);
numbers.Add(4);
foreach (var number in query)
{
Console.WriteLine(number);
}
実行結果は次のようになります。
2
3
4
queryを作った時点では4は存在していませんでした。しかし、foreachで列挙する前にnumbers.Add(4)を実行しているため、結果に4も含まれます。
この挙動を避けたい場合は、ToList()などで結果を先に確定します。
C#var result = numbers.Where(n => n >= 2).ToList();
4-2. 同じクエリを複数回実行してしまう
遅延実行のクエリは、列挙するたびに処理が実行されます。
C#var query = numbers.Where(n =>
{
Console.WriteLine($"{n}を判定");
return n % 2 == 0;
});
foreach (var number in query)
{
Console.WriteLine(number);
}
foreach (var number in query)
{
Console.WriteLine(number);
}
この場合、Whereの判定処理は2回実行されます。
データ量が少なければ問題になりにくいですが、重い処理やデータベースアクセスを含む場合は、パフォーマンス低下の原因になります。
複数回使う場合は、次のように一度ToList()で確定しておくと安全です。
C#var result = query.ToList();
4-3. 例外がクエリ定義時ではなく実行時に発生する
遅延実行では、例外が発生するタイミングにも注意が必要です。
C#var numbers = new List<int> { 10, 5, 0 };
var query = numbers.Select(n => 100 / n);
Console.WriteLine("クエリ作成完了");
foreach (var number in query)
{
Console.WriteLine(number);
}
このコードでは、Selectを書いた時点では例外は発生しません。
foreachで列挙し、nが0になったタイミングでDivideByZeroExceptionが発生します。
つまり、エラーの原因がクエリ定義部分にあっても、実際に例外が出るのは後のタイミングです。
4-4. デバッグ時に処理の流れがわかりにくい
遅延実行は便利ですが、デバッグ時には処理の流れがわかりにくくなることがあります。
C#var query = users
.Where(u => u.IsActive)
.Select(u => u.Name);
このコードだけを見ると、すぐにユーザーが絞り込まれて名前が取り出されるように見えます。しかし実際には、列挙されるまで処理されません。
デバッグ時には、次のようにToList()を一時的に使うと、結果を確認しやすくなります。
C#var result = users
.Where(u => u.IsActive)
.Select(u => u.Name)
.ToList();
ただし、本番コードでむやみにToList()を使うとメモリ使用量が増えることもあるため、必要な場所だけで使うことが大切です。
4-5. 外部変数の値が後から変わると結果に影響する
LINQの条件式で外部変数を使う場合、その変数の値が後から変わると結果に影響します。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
int threshold = 3;
var query = numbers.Where(n => n > threshold);
threshold = 1;
foreach (var number in query)
{
Console.WriteLine(number);
}
実行結果は次のようになります。
2
3
4
5
クエリを作成した時点ではthresholdは3でしたが、列挙する前に1へ変更されています。遅延実行では、実行時点の変数の値が使われるため、n > 1として評価されます。
クエリ作成時点の値で固定したい場合は、別の変数に値をコピーする、またはToList()で結果を確定するなどの対策が必要です。
5. 遅延実行によるメリット
5-1. 必要な分だけ処理できる
遅延実行の大きなメリットは、必要な分だけ処理できることです。
たとえば、条件に合う最初の1件だけが欲しい場合、すべての要素を処理する必要はありません。
C#var first = numbers
.Where(n => n % 2 == 0)
.First();
このコードでは、最初の偶数が見つかった時点で処理を終了できます。
大量のデータを扱う場合、不要な処理を避けられるため効率的です。
5-2. メモリ使用量を抑えられる
遅延実行では、結果をすべて一度にメモリへ展開しなくても処理できます。
C#var query = largeData
.Where(x => x.IsValid)
.Select(x => x.Name);
このようなコードでは、ToList()を呼ばない限り、結果全体をリストとして作成しません。列挙しながら1件ずつ処理できるため、メモリ使用量を抑えやすくなります。
特に、大量のログ、ファイル行、データベース結果などを扱う場合に有効です。
5-3. 無駄な処理を減らしてパフォーマンスを改善できる
遅延実行は、後続の処理と組み合わせることで無駄な処理を減らせます。
C#var result = numbers
.Where(n => n % 2 == 0)
.Take(3);
このコードでは、偶数を3件取得できた時点で処理を終了できます。すべての偶数を探してから3件取り出すのではなく、必要な件数に達したところで止められます。
これは、遅延実行とTakeの組み合わせによる代表的なメリットです。
5-4. TakeやFirstと組み合わせた効率的な使い方
TakeやFirstは、遅延実行と相性のよいメソッドです。
C#var topUsers = users
.Where(u => u.IsActive)
.Take(10);
この場合、アクティブなユーザーを10件取得できれば、それ以上の処理は不要です。
また、次のようにFirstOrDefaultを使えば、条件に合う最初の要素だけを効率的に探せます。
C#var user = users
.Where(u => u.Id == targetId)
.FirstOrDefault();
さらに簡潔に書くなら、次のようにもできます。
C#var user = users.FirstOrDefault(u => u.Id == targetId);
必要な分だけ処理するという考え方は、C#の遅延実行を活かすうえで重要です。
6. 遅延実行を避けたい場合の対処法
6-1. ToListやToArrayで結果を確定する
遅延実行を避けたい場合は、ToList()やToArray()を使って結果を確定します。
C#var result = numbers
.Where(n => n % 2 == 0)
.ToList();
このコードでは、ToList()を呼び出した時点でWhereが実行され、偶数だけを含むリストが作成されます。
結果を確定しておけば、後から元データが変更されても、resultの内容は変わりません。
C#numbers.Add(6);
foreach (var number in result)
{
Console.WriteLine(number);
}
この場合、resultにはToList()した時点の結果だけが入っています。
6-2. 複数回使うクエリは一度変数に保存する
同じクエリ結果を複数回使う場合は、遅延実行のままではなく、リストなどに保存するのがおすすめです。
悪い例は次のようなコードです。
C#var query = users.Where(u => u.IsActive);
var count = query.Count();
foreach (var user in query)
{
Console.WriteLine(user.Name);
}
この場合、Count()で1回、foreachで1回、合計2回クエリが実行されます。
改善するなら、次のように書きます。
C#var activeUsers = users
.Where(u => u.IsActive)
.ToList();
var count = activeUsers.Count;
foreach (var user in activeUsers)
{
Console.WriteLine(user.Name);
}
これなら、条件判定はToList()のタイミングで1回だけ実行されます。
6-3. 意図しない再評価を防ぐ書き方
意図しない再評価を防ぐには、「この結果は固定したいのか」「毎回最新の元データを見たいのか」を意識することが大切です。
結果を固定したい場合は、ToList()を使います。
C#var fixedResult = numbers
.Where(n => n > 10)
.ToList();
一方、元データの最新状態を毎回反映したい場合は、遅延実行のままにしておくことにも意味があります。
C#var query = numbers.Where(n => n > 10);
このように、遅延実行そのものが悪いわけではありません。意図に合っていない使い方をすると問題になる、という点が重要です。
6-4. パフォーマンス低下を防ぐ実装例
パフォーマンス低下を防ぐためには、重いクエリを何度も実行しないようにします。
C#var expensiveQuery = items
.Where(x => IsExpensiveCheck(x))
.Select(x => ConvertItem(x));
このクエリを複数回使うなら、次のように一度だけ実行します。
C#var cachedItems = items
.Where(x => IsExpensiveCheck(x))
.Select(x => ConvertItem(x))
.ToList();
Console.WriteLine(cachedItems.Count);
foreach (var item in cachedItems)
{
Console.WriteLine(item);
}
IsExpensiveCheckやConvertItemが重い処理の場合、再実行を防ぐだけでパフォーマンスが大きく改善することがあります。
7. IEnumerableとIQueryableにおける遅延実行の違い
7-1. IEnumerableの遅延実行
IEnumerable<T>は、主にメモリ上のコレクションを列挙するためのインターフェースです。
C#IEnumerable<int> query = numbers.Where(n => n % 2 == 0);
この場合、numbersがList<int>などのメモリ上のデータであれば、foreachやToList()で列挙したタイミングでC#の処理として実行されます。
IEnumerable<T>のLINQは、基本的にアプリケーション側で処理されます。
7-2. IQueryableの遅延実行
IQueryable<T>は、データベースなど外部のデータソースに対するクエリでよく使われます。
Entity Frameworkを使う場合、次のようなコードが典型例です。
C#IQueryable<User> query = dbContext.Users
.Where(u => u.IsActive);
この時点では、まだデータベースにSQLは発行されていません。
IQueryable<T>では、LINQの式が式木として保持され、実行時にSQLなどへ変換されます。
7-3. Entity FrameworkでSQLが発行されるタイミング
Entity Frameworkでは、次のようなタイミングでSQLが発行されます。
C#var users = dbContext.Users
.Where(u => u.IsActive)
.ToList();
この場合、ToList()を呼び出した時点でSQLが発行され、データベースから結果を取得します。
また、次のような場合もSQLが発行されます。
C#var count = dbContext.Users.Count();
var first = dbContext.Users.FirstOrDefault();
foreach (var user in dbContext.Users.Where(u => u.IsActive))
{
Console.WriteLine(user.Name);
}
Count()、FirstOrDefault()、foreachなど、結果が必要になったタイミングで実行される点はIEnumerable<T>と似ています。
ただし、IQueryable<T>の場合は、処理がC#上で行われるのではなく、SQLに変換されてデータベース側で実行されることが多い点が大きな違いです。
7-4. AsEnumerableとToListの違い
AsEnumerable()とToList()は混同されやすいですが、役割が違います。
AsEnumerable()は、型をIEnumerable<T>として扱うためのメソッドです。基本的に、その場で結果をリスト化するわけではありません。
C#var query = dbContext.Users
.Where(u => u.IsActive)
.AsEnumerable();
この時点では、まだ列挙されていなければ実行は確定していません。ただし、以降のLINQ処理はIEnumerable<T>として扱われるため、データベース側ではなくアプリケーション側で処理される可能性があります。
一方、ToList()はその場でクエリを実行し、結果をリストとして確定します。
C#var list = dbContext.Users
.Where(u => u.IsActive)
.ToList();
AsEnumerable()は「ここから先はEnumerableとして扱う」、ToList()は「ここで実行して結果を確定する」と考えるとわかりやすいです。
8. 遅延実行を理解するための実践コード例
8-1. Console.WriteLineで実行タイミングを確認する
遅延実行を理解するには、Console.WriteLineを使って確認するのがわかりやすいです。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(n =>
{
Console.WriteLine($"Where: {n}");
return n % 2 == 0;
});
Console.WriteLine("foreachの前");
foreach (var number in query)
{
Console.WriteLine($"Result: {number}");
}
実行結果は次のようになります。
foreachの前
Where: 1
Where: 2
Result: 2
Where: 3
Where: 4
Result: 4
Where: 5
foreachの前にはWhereが実行されていないことが確認できます。
8-2. WhereとSelectの処理順を確認する
WhereとSelectを組み合わせた場合、処理は要素ごとに進みます。
C#var query = numbers
.Where(n =>
{
Console.WriteLine($"Where: {n}");
return n % 2 == 0;
})
.Select(n =>
{
Console.WriteLine($"Select: {n}");
return n * 10;
});
foreach (var number in query)
{
Console.WriteLine($"Result: {number}");
}
実行結果は次のようになります。
Where: 1
Where: 2
Select: 2
Result: 20
Where: 3
Where: 4
Select: 4
Result: 40
Where: 5
すべてのWhereが終わってからSelectが実行されるのではなく、条件に合った要素ごとにSelectが実行されています。
8-3. ToListで実行タイミングが変わる例
ToList()を途中に入れると、その時点で処理が実行されます。
C#var query = numbers
.Where(n =>
{
Console.WriteLine($"Where: {n}");
return n % 2 == 0;
})
.ToList();
Console.WriteLine("ToListの後");
foreach (var number in query)
{
Console.WriteLine($"Result: {number}");
}
実行結果は次のようになります。
Where: 1
Where: 2
Where: 3
Where: 4
Where: 5
ToListの後
Result: 2
Result: 4
ToList()の時点でWhereが実行されているため、foreachではすでに確定済みのリストを表示しているだけです。
8-4. 複数回列挙による再実行の例
遅延実行のクエリは、複数回列挙するとそのたびに実行されます。
C#var query = numbers.Where(n =>
{
Console.WriteLine($"判定: {n}");
return n % 2 == 0;
});
Console.WriteLine("1回目");
foreach (var number in query)
{
Console.WriteLine(number);
}
Console.WriteLine("2回目");
foreach (var number in query)
{
Console.WriteLine(number);
}
このコードでは、判定の表示が2回分出力されます。
再実行を避けたい場合は、次のようにします。
C#var result = numbers
.Where(n =>
{
Console.WriteLine($"判定: {n}");
return n % 2 == 0;
})
.ToList();
Console.WriteLine("1回目");
foreach (var number in result)
{
Console.WriteLine(number);
}
Console.WriteLine("2回目");
foreach (var number in result)
{
Console.WriteLine(number);
}
この場合、判定処理はToList()のタイミングで1回だけ実行されます。
9. C#の遅延実行でよくある質問
9-1. LINQはすべて遅延実行される?
LINQがすべて遅延実行されるわけではありません。
WhereやSelectのように遅延実行されるメソッドもあれば、ToList、Count、First、Sumのように即時実行されるメソッドもあります。
見分けるポイントは、戻り値です。
IEnumerable<T>やIQueryable<T>を返すメソッドは遅延実行されることが多く、具体的な値やコレクションを返すメソッドは即時実行されることが多いです。
9-2. ToListはいつ使うべき?
ToList()は、結果をその時点で確定したいときに使います。
たとえば、次のような場合です。
元データが後から変わっても結果を固定したい
同じクエリ結果を複数回使いたい
デバッグで中身を確認したい
Entity Frameworkで明確にSQLを実行したい
後続処理でリストとして扱いたい
ただし、必要がない場面でToList()を使いすぎると、メモリ使用量が増えたり、不要な全件取得が発生したりします。
「結果を確定する必要があるか」を考えて使うことが大切です。
9-3. foreachしないと実行されないのはなぜ?
WhereやSelectなどは、結果をすぐに作るのではなく、「列挙されたらどう処理するか」を表すオブジェクトを返すためです。
foreachは、そのオブジェクトから実際に要素を1つずつ取り出します。この取り出しのタイミングで、LINQの処理が実行されます。
つまり、foreachは遅延実行のクエリに対して「実際に結果をください」と要求しているようなものです。
9-4. 遅延実行はパフォーマンスに悪い?
遅延実行そのものがパフォーマンスに悪いわけではありません。
むしろ、必要な分だけ処理できる、メモリ使用量を抑えられる、TakeやFirstで早めに処理を終了できるなど、多くのメリットがあります。
ただし、同じクエリを何度も列挙したり、意図せずデータベースに何度もアクセスしたりすると、パフォーマンスが悪化します。
遅延実行は、正しく使えば効率的です。問題になるのは、実行タイミングを意識せずに使った場合です。
9-5. 遅延実行と遅延評価は同じ意味?
C#やLINQの文脈では、遅延実行と遅延評価はほぼ同じ意味で使われることがあります。
どちらも「必要になるまで処理や評価を行わない」という考え方です。
ただし、厳密には、遅延評価は式の評価を必要になるまで遅らせる一般的な考え方で、遅延実行はLINQなどの処理実行タイミングに焦点を当てた表現として使われることが多いです。
C#のLINQを学ぶうえでは、「クエリを書いただけでは実行されず、列挙やToList()などで初めて実行される」と理解しておけば問題ありません。
まとめ
C#の遅延実行とは、LINQのクエリを定義した時点では処理を実行せず、実際に結果が必要になったタイミングで実行する仕組みです。
WhereやSelectを書いただけでは処理されず、foreachで列挙したり、ToList()、Count()、First()などを呼び出したりしたタイミングで実行されます。
遅延実行には、必要な分だけ処理できる、メモリ使用量を抑えられる、無駄な処理を減らせるといったメリットがあります。一方で、元データの変更が結果に影響する、同じクエリが複数回実行される、例外の発生タイミングがわかりにくいといった注意点もあります。
特にEntity FrameworkなどでIQueryable<T>を扱う場合は、SQLがいつ発行されるかを意識することが重要です。
C#でLINQを安全に使うには、「このクエリはまだ実行されていないのか」「どのタイミングで結果を確定するのか」を考えながら書くことが大切です。遅延実行を正しく理解すれば、LINQをより効率的で読みやすく、パフォーマンスのよいコードとして活用できます。

