foreach の内部動作を徹底解剖 ― C# コンパイラは舞台裏で何をしているのか?
目次
TL;DR
- foreach は 構文糖衣。コンパイラが GetEnumerator()・MoveNext()・Current(必要なら Dispose())を呼び出すコードに変換する。
- IEnumerable 実装は必須ではなく、“パターン”(同名メンバーを持つだけ)でも動く。
- 配列は特別扱いで 単純な for ループ に書き換わり、ボクシングが発生しない。
- await foreach は非同期版 (GetAsyncEnumerator / MoveNextAsync) を呼び出す。
- パフォーマンス最適化:構造体(struct)列挙子や Span<T> を使えば割り当てゼロで回せる。
1. なぜ foreach を使うのか
- インデックスを意識せず可読性が高い
- try/finally による確実なリソース解放(IDisposable)を自動化
- 型安全(ジェネリックでキャスト不要)
2. コンパイラが生成するコードを覗く
ソース
foreach (var n in numbers)
{
Console.WriteLine(n);
}
実際に生成される C#(擬似コード)
using (var e = numbers.GetEnumerator()) // ① 列挙子取得
{
while (e.MoveNext()) // ② 次の要素へ
{
var n = e.Current; // ③ 現在要素
Console.WriteLine(n);
} // ④ finally で Dispose()
}
- using 相当の try/finally が必ず挿入される。
3. 列挙パターンの 4 要件
要件 | 役割 |
---|---|
public GetEnumerator() | 列挙子を返す(拡張メソッドでも可) |
bool MoveNext() | 次へ進み、要素があれば true |
Current プロパティ | 現在要素を返す |
Dispose() (任意) | 後片付け |
これら 名前さえ合えば インターフェース実装なしでも foreach は可能(いわゆるダックタイピング)。
C# 9 では拡張メソッド版 GetEnumerator() も正式サポート。
4. 配列・Span<T>・List<T> ― 型ごとの最適化
型 | 変換後 | 特徴 |
---|---|---|
配列 | for (int i = 0; i < arr.Length; i++) … | ボクシングなし、最速 |
Span<T> | インライン化された ref 構造体列挙子 | GC ヒープ割り当てゼロ |
汎用コレクション | IEnumerator<T> | 要素数不明でも走査可 |
5. yield return とカスタム列挙子
yield return 付きメソッドは、コンパイラが ステートマシン クラス(列挙子)を自動生成。ローカル変数や戻り位置を保持して連続呼び出しに対応する。
public static IEnumerable<int> Range(int start, int count)
{
for (int i = 0; i < count; i++)
yield return start + i;
}
6. await foreach と非同期列挙
- 必要メンバーは GetAsyncEnumerator / MoveNextAsync / Current
- MoveNextAsync は Task<bool> または ValueTask<bool> を返す。
- データストリームや I/O バウンド処理との相性が良い。
7. パフォーマンス指標とベンチマークの勘所
- ヒープ割り当て:クラス列挙子は GC 負荷;struct 列挙子で回避
- 境界チェック:配列 for は JIT 最適化で Bounds Check Hoisting(BCH)が効く
- インライン展開:手書き for より列挙子呼び出しがボトルネックになる場合あり
- Span や Ref‐Readonly 列挙(C# 7.3~)でゼロコストを狙う
8. ありがちな落とし穴
症状 | 原因/対策 |
---|---|
コレクションを走査中に変更すると InvalidOperationException | 変更しない or for + インデックス |
Dispose されない 独自列挙子 | IDisposable 実装を忘れずに |
ボクシングが発生 | 値型コレクション+非ジェネリック列挙子に注意 |
foreach vs for どちらが速い? | 小規模配列なら for が有利、可読性とトレードオフで選択 |
9. まとめ & 練習課題
- foreach は コンパイル時に展開される構文糖衣。舞台裏を理解すると自作コレクションやパフォーマンスチューニングに直結。
- 課題
- 10 要素の int[] を foreach と for で回し、BenchmarkDotNet で速度差を計測せよ。
- yield return を使わずに、自分で IEnumerator<int> を実装してみよう。
- await foreach で GitHub API からページネーション付きにリポジトリ一覧を流すサンプルを書き、キャンセル処理を組み込め。
これで “黒魔術” と言われがちな foreach の裏側が見えるはずです。内部実装を知ることで、コードレビューや最適化の説得力が一段アップします。
訪問数 2 回, 今日の訪問数 2回
ディスカッション
コメント一覧
まだ、コメントがありません