C#において配列の要素を順番に処理する際、最も頻繁に利用されるのがforeach文です。
配列の全要素を走査するという単純なタスクにおいて、foreachは記述の簡潔さと安全性を両立させた優れた構文です。
しかし、近年の.NETの進化に伴い、単に「書きやすい」という理由だけでなく、パフォーマンスの観点からもforeachが最適解となるケースが増えています。
本記事では、foreachの基本的な使い方から、内部動作の仕組み、for文との使い分け、そして最新のC#におけるSpan<T>を活用した超高速なループ処理までを徹底的に解説します。
C#におけるforeach文の基本と配列の処理
C#のforeach文は、配列やコレクションの要素を先頭から順に取得し、繰り返し処理を行うための反復ステートメントです。
従来のfor文のようにループカウンタ(インデックス)を管理する必要がないため、コードの可読性が飛躍的に向上し、インデックスの指定ミスによるバグ(境界外アクセスなど)を未然に防ぐことができます。
基本的な構文と実装例
配列に対してforeachを使用する際の最も標準的な実装は以下の通りです。
using System;
public class Program
{
public static void Main()
{
// 整数の配列を定義
int[] numbers = { 10, 20, 30, 40, 50 };
Console.WriteLine("配列の要素を表示します:");
// foreach文による反復処理
foreach (int number in numbers)
{
// 各要素に対する処理
Console.WriteLine($"値: {number}");
}
}
}
配列の要素を表示します:
値: 10
値: 20
値: 30
値: 40
値: 50
この例では、int[] 型の配列から一つずつ要素を取り出し、変数 number に代入してコンソールに出力しています。
foreach文内で宣言されたループ変数は、その反復内では読み取り専用として扱われるのが基本原則です。
型推論(var)の活用
C#の型推論機能である var キーワードを使用することで、より柔軟に記述することが可能です。
特に複雑なクラス型の配列を扱う場合、コードを簡潔に保つために推奨されます。
var fruits = new string[] { "Apple", "Banana", "Cherry" };
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}
foreach文の内部動作とコンパイラ最適化
多くの開発者が「foreachはIEnumeratorを生成するため、for文よりも遅い」というイメージを持っています。
しかし、配列に対するforeachに関しては、この認識は誤りです。
配列における「特殊扱い」
通常、foreachは IEnumerable インターフェースの GetEnumerator() メソッドを呼び出し、列挙子オブジェクトを介して処理を行います。
しかし、対象が配列(System.Array)の場合、C#コンパイラは実行効率を最大化するために、内部的に標準的なfor文へと変換します。
例えば、先ほどの foreach (int number in numbers) というコードは、コンパイル時に以下のような低レベルなコードと等価なIL(中間言語)に変換されます。
// コンパイラが生成するイメージ
int[] array = numbers;
for (int i = 0; i < array.Length; i++)
{
int number = array[i];
// 処理内容
}
このように、配列のforeachは列挙子オブジェクトのヒープ割り当てが発生しないため、オーバーヘッドが極めて低く抑えられています。
境界チェックの除去(Bounds Check Elimination)
JIT(Just-In-Time)コンパイラは、配列のループ処理をさらに最適化します。
for文でループカウンタを使用する場合、アクセスごとに「インデックスが配列の範囲内か」というチェックが行われます。
しかし、foreach形式(または配列のLengthを上限としたfor)では、コンパイラが「範囲外になることはありえない」と判断し、境界チェックを省略する最適化(BCE)を適用することがあります。
これにより、手動でインデックスを操作するよりも高速に動作するケースが存在します。
foreachとforの比較:どちらを使うべきか
開発現場では、foreachとforのどちらを採用すべきか議論になることがあります。
それぞれの特性を理解し、適切に使い分けることが重要です。
foreachのメリット・デメリット
| 特徴 | 詳細 |
|---|---|
| 可読性 | 非常に高い。意図が明確になる。 |
| 安全性 | インデックス操作ミス(オフバイワンエラー)が発生しない。 |
| 制限 | 要素の値を直接書き換えることができない(※Spanを使用しない場合)。 |
| 制限 | 現在のインデックス番号を直接取得できない。 |
forのメリット・デメリット
| 特徴 | 詳細 |
|---|---|
| 柔軟性 | 逆順ループ、ステップ実行(2つ飛ばしなど)が可能。 |
| 編集 | ループ内で配列の要素を直接書き換えることができる。 |
| 複雑性 | 記述量が増え、インデックス管理によるバグの温床になりやすい。 |
使い分けの指針
基本的には、「配列の全要素を単に読み取るだけならforeach」を第一選択にすべきです。
インデックスが必要な場合や、特定の条件でインデックスをスキップする必要がある場合にのみ、for文の使用を検討してください。
もし「何番目の要素か」という情報が必要なだけであれば、以下のように外部にカウンタを用意する方法もありますが、複雑になるようならfor文の方が適しています。
int index = 0;
foreach (var item in array)
{
Console.WriteLine($"{index}: {item}");
index++;
}
Span<T> を活用した最新のループ処理
C# 7.2で導入され、.NET 8/9以降でさらに強化された Span<T> および ReadOnlySpan<T> は、配列のループ処理に革命をもたらしました。
Spanは「メモリの連続した領域」を抽象化した構造体であり、スタック上に割り当てられるため、非常に高速なアクセスが可能です。
配列をSpanとして扱う
配列をSpanに変換してforeachで回す手法は、特に高頻度で実行されるロジックにおいてパフォーマンス上の利点があります。
using System;
public class Program
{
public static void Main()
{
int[] data = { 1, 2, 3, 4, 5 };
// 配列をSpanに変換
Span<int> spanData = data.AsSpan();
// Spanに対するforeach
foreach (ref int value in spanData)
{
// refキーワードを使うことで、要素の値を直接書き換え可能!
value *= 2;
}
// 結果の確認
foreach (var v in data)
{
Console.Write($"{v} ");
}
}
}
2 4 6 8 10
ref foreachの威力
上記のコードで注目すべきは、foreach (ref int value in spanData) という記述です。
従来の配列に対するforeachでは要素の書き換えは不可能でしたが、Spanとrefキーワードを組み合わせることで、読み取り専用という制約を突破し、直接要素を編集できるようになります。
これは、大きな構造体の配列を扱う際に、コピーコストを抑えつつ値を更新する場面で極めて有効です。
性能を極限まで高めるためのテクニック
エンタープライズレベルのアプリケーションやゲーム開発など、マイクロ秒単位の最適化が求められるシーンでは、以下の点に留意してください。
多次元配列とジャグ配列(配列の配列)
C#には多次元配列(int[,])とジャグ配列(int[][])がありますが、パフォーマンス面ではジャグ配列が圧倒的に有利です。
多次元配列のforeachは、内部的に複雑なインデックス計算を行うため、JIT最適化が効きにくい傾向があります。
一方、ジャグ配列は「配列の配列」であるため、各階層が単一の配列としてBCE(境界チェック除去)の対象になります。
IEnumerable<T>へのキャストを避ける
配列を直接foreachに渡すのではなく、IEnumerable<T> インターフェース型の変数に代入してからforeachを行うと、前述の「for文への最適化」が働かなくなります。
int[] numbers = { 1, 2, 3 };
// 最適化される(高速)
foreach (var n in numbers) { ... }
// 最適化されない(低速:IEnumeratorが生成される)
IEnumerable<int> enumerableNumbers = numbers;
foreach (var n in enumerableNumbers) { ... }
このように、型を抽象化しすぎるとパフォーマンスが低下するため、パフォーマンスが重要な箇所では具体的な配列型またはSpan<T>として処理するのが鉄則です。
多次元配列とforeachの特殊な挙動
C#の配列において、多次元配列に対するforeachの挙動は少し特殊です。
多次元配列をforeachで回すと、すべての次元がフラット化されて列挙されます。
int[,] matrix = {
{ 1, 2 },
{ 3, 4 }
};
foreach (int val in matrix)
{
Console.WriteLine(val);
}
1
2
3
4
入れ子になったfor文を書く必要がないため便利な反面、行と列を意識した処理には向きません。
また、前述の通り多次元配列の列挙はジャグ配列に比べて低速であるため、大量のデータを扱う場合は設計段階でジャグ配列(T[][])の採用を検討すべきです。
LINQのForEachとの違い
リストや配列に対して .ToList().ForEach() や、一部のライブラリが提供する Array.ForEach を使用するケースも見られます。
しかし、これらは言語構文としてのforeachではなく、デリゲートを呼び出すメソッドです。
// LINQ/Arrayメソッド形式
Array.ForEach(numbers, n => Console.WriteLine(n));
この形式は関数型プログラミングのスタイルに近く、1行で記述できるメリットはありますが、反復ごとにデリゲート呼び出しのオーバーヘッドが発生します。
また、ループ内での break や continue が使用できないという制約もあります。
基本的には通常のforeach文を使用するのが、モダンC#における標準的な作法です。
まとめ
C#のforeach文は、単なるシンタックスシュガー(書きやすくするための構文)を超えた、非常に強力な反復手段です。
特に配列に対しては、コンパイラとJITによる高度な最適化が施されるため、初心者からシニアエンジニアまで安心して使用できる基盤となっています。
本記事のポイントを振り返ります:
- 配列のforeachはコンパイル時にfor文へ変換されるため、非常に高速である。
- 境界チェックの自動省略(BCE)により、手動のfor文よりも効率的な場合がある。
- Span<T>と組み合わせることで、スタック上での高速なアクセスと要素の書き換えが可能になる。
- 基本はforeach、特殊なインデックス操作が必要な時のみforを選択する。
最新の.NET環境では、メモリ効率と実行速度の両立がますます重視されています。
まずは読みやすく安全なforeachから書き始め、ボトルネックが判明した場合には Span<T> や ref foreach を導入するという段階的なアプローチを推奨します。
この記事で紹介したテクニックを駆使して、クリーンで高パフォーマンスなC#コードを記述していきましょう。






