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 は コンパイル時に展開される構文糖衣。舞台裏を理解すると自作コレクションやパフォーマンスチューニングに直結。
  • 課題
    1. 10 要素の int[] を foreach と for で回し、BenchmarkDotNet で速度差を計測せよ。
    2. yield return を使わずに、自分で IEnumerator<int> を実装してみよう。
    3. await foreach で GitHub API からページネーション付きにリポジトリ一覧を流すサンプルを書き、キャンセル処理を組み込め。

これで “黒魔術” と言われがちな foreach の裏側が見えるはずです。内部実装を知ることで、コードレビューや最適化の説得力が一段アップします。

訪問数 2 回, 今日の訪問数 2回

C#,foreach

Posted by hidepon