コレクションの全要素に対して同じ処理を行うとき、C# では foreach
ループがもっとも読みやすく、安全な選択肢です。
本記事では、配列・List・Dictionary といった代表的なコレクションに対する基本的な使い方から、制御構文、よくあるエラー対策、内部仕組み、パフォーマンスのコツ、そして C# 8 で追加された await foreach
まで、初心者の方にも分かりやすく実用的に解説します。
C# foreach ループとは?コレクションを反復処理する基本
foreach の目的とメリット(可読性・安全性)
foreach
は、コレクション(配列、List、Dictionary、LINQ の結果など)の全要素を順番に取り出して処理するための構文です。
インデックス管理や範囲チェックを手書きする必要がないため、コードが簡潔になり、境界外アクセスやインデックスのオフバイワンエラーを避けやすくなります。
また、foreach
はコレクションの走査中に不正な変更が行われないよう、実装によっては検出して例外を投げるため、意図しないバグの早期発見にも役立ちます。
代表的な適用対象
配列、List<T>
、Dictionary<TKey, TValue>
、HashSet<T>
、Queue<T>
、Stack<T>
、IEnumerable<T>
を返す LINQ の結果など、ほぼすべての反復可能な型で利用できます。
foreach の基本構文と書き方(C# の記法)
foreach
の基本構文は次のとおりです。
要素型は var
または正確な型で受け取れます。
// 基本構文
// foreach (要素型 要素変数 in コレクション) { 処理 }
foreach (var item in collection)
{
// item を使った処理
}
// 例:int 配列を走査して合計を出す
using System;
class Program
{
static void Main()
{
int[] numbers = { 1, 3, 5, 7 };
int sum = 0;
foreach (int n in numbers) // n は配列の各要素(読み取り用の変数)
{
sum += n;
}
Console.WriteLine($"合計: {sum}");
}
}
合計: 16
補足として、C# では foreach
変数は原則読み取り専用であり、その値を代入で書き換えてもコレクションに反映されません(後述)。
C# foreach ループの基本的な使い方
配列(int[]/string[])を foreach で回す
int 配列の例
using System;
class Program
{
static void Main()
{
int[] scores = { 80, 92, 75 };
int index = 0;
foreach (var score in scores)
{
Console.WriteLine($"[{index}] = {score}");
index++;
}
}
}
[0] = 80
[1] = 92
[2] = 75
string 配列の例
using System;
class Program
{
static void Main()
{
string[] names = { "Alice", "Bob", "Charlie" };
foreach (string name in names)
{
Console.WriteLine($"Hello, {name}!");
}
}
}
Hello, Alice!
Hello, Bob!
Hello, Charlie!
List<T> を foreach で回す
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var fruits = new List<string> { "Apple", "Banana", "Cherry" };
foreach (var f in fruits)
{
Console.WriteLine(f.ToUpper()); // 各要素を大文字で表示
}
}
}
APPLE
BANANA
CHERRY
Dictionary<TKey, TValue> を foreach で回す(KeyValuePair の扱い)
Dictionary
は KeyValuePair<TKey, TValue>
の列として走査されます。
キーや値だけを取りたい場合は Keys
/ Values
を使います。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var ages = new Dictionary<string, int>
{
["Alice"] = 23,
["Bob"] = 34,
["Charlie"] = 28
};
// KeyValuePair を受け取る
foreach (var kvp in ages)
{
Console.WriteLine($"{kvp.Key} is {kvp.Value} years old.");
}
// Keys だけ
foreach (var name in ages.Keys)
{
Console.WriteLine($"Name: {name}");
}
// Values だけ
foreach (var age in ages.Values)
{
Console.WriteLine($"Age: {age}");
}
}
}
Alice is 23 years old.
Bob is 34 years old.
Charlie is 28 years old.
Name: Alice
Name: Bob
Name: Charlie
Age: 23
Age: 34
Age: 28
foreach と for/while の違いと使い分け
可読性・保守性の比較(foreach ループの強み)
foreach
は「要素を1つずつ処理する」という意図を直接表現でき、境界チェックの記述やインデックス変数の管理が不要なため、バグを減らし保守性を高めます。
特にコレクションの種類が変わってもコードがほぼ変わらない点は長期運用で有利です。
観点 | foreach | for | while |
---|---|---|---|
可読性 | 高い(意図が明確) | 中(インデックス管理が必要) | 中(条件次第で読みにくくなる) |
安全性 | 走査中の変更を検出(例外) | 自由(変更は自己責任) | 自由(条件ミスで無限ループの恐れ) |
インデックスアクセス | 不可(別途管理が必要) | 可能 | 可能(自前で増減) |
速度 | 多くのケースで十分高速 | 配列に対しては最速級 | ケースバイケース |
用途 | 単純な全件処理 | インデックスが必要な処理 | 条件駆動の柔軟なループ |
インデックスが必要な場合の選択肢(for との比較)
インデックスが必要なら for
が素直です。
foreach
でカウンタを別途持つことも可能ですが、同期ミスの余地が増えます。
// for でインデックスと値の両方を使う
using System;
class Program
{
static void Main()
{
string[] names = { "A", "B", "C" };
for (int i = 0; i < names.Length; i++)
{
Console.WriteLine($"{i}: {names[i]}");
}
}
}
0: A
1: B
2: C
// foreach にカウンタを併用(やむを得ない場合)
using System;
class Program
{
static void Main()
{
string[] names = { "A", "B", "C" };
int i = 0;
foreach (var name in names)
{
Console.WriteLine($"{i}: {name}");
i++;
}
}
}
0: A
1: B
2: C
補足: LINQ の Select((value, index) => ...)
でインデックスを得る方法もありますが、軽負荷の箇所や可読性重視の場合に限定し、性能が重要な内側ループでは避けるのが無難です。
foreach で使える制御構文と書き方
break・continue・return の使い方
break
はループ自体を終了、continue
は次の要素にスキップ、return
はメソッドの終了です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var nums = new List<int> { 1, 2, 3, 4, 5, 6 };
Console.WriteLine("偶数だけ表示。4 を見たら終了。");
foreach (var n in nums)
{
if (n % 2 != 0) continue; // 奇数はスキップ
if (n == 4) break; // 4 が来たらループ終了
Console.WriteLine(n);
}
Console.WriteLine("メソッドから早期 return する例: " + SumUntil(nums, 3));
}
static int SumUntil(IEnumerable<int> source, int stopAt)
{
int sum = 0;
foreach (var n in source)
{
if (n == stopAt) return sum; // 条件でメソッド終了
sum += n;
}
return sum;
}
}
偶数だけ表示。4 を見たら終了。
2
メソッドから早期 return する例: 3
ネストした foreach の注意点(スコープと可読性)
ネストが深くなると読みづらいため、変数名を具体化し、処理をメソッドに切り出すと可読性が上がります。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var groups = new List<List<string>>
{
new() { "A1", "A2" },
new() { "B1" },
new() { "C1", "C2", "C3" }
};
foreach (var group in groups)
{
Console.WriteLine("Group:");
foreach (var item in group)
{
Console.WriteLine($" - {item}");
}
}
}
}
Group:
- A1
- A2
Group:
- B1
Group:
- C1
- C2
- C3
foreach の注意点とよくあるエラー対策
反復中にコレクションを変更できない(InvalidOperationException)
多くのコレクション(List<T>
など)は、foreach
のループ中に要素を追加・削除すると InvalidOperationException
を投げます。
// 悪い例:ループ中に Add する
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var list = new List<int> { 1, 2, 3 };
try
{
foreach (var n in list)
{
if (n == 2)
{
list.Add(99); // ここで例外
}
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine("例外: " + ex.GetType().Name);
}
}
}
例外: InvalidOperationException
安全な代替案は「事前に変更点を記録して後でまとめて変更」「変更対象を別リストに集める」「for
で後ろから削除」「RemoveAll
を使う」などです。
// 安全な削除(後ろから for)
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var list = new List<int> { 1, 2, 3, 2, 4 };
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] == 2)
{
list.RemoveAt(i);
}
}
Console.WriteLine(string.Join(", ", list));
}
}
1, 3, 4
null と空コレクションの安全な扱い
foreach
は空コレクションなら何もせず終了しますが、null
に対しては NullReferenceException
になります。
??
演算子で空コレクションに差し替えるのが安全です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> items = null;
// IEnumerable<T> として扱えるなら Enumerable.Empty<T>() が便利
foreach (var s in items ?? Enumerable.Empty<string>())
{
Console.WriteLine(s);
}
// 配列なら Array.Empty<T>() も有効
string[] arr = null;
foreach (var s in arr ?? Array.Empty<string>())
{
Console.WriteLine(s);
}
Console.WriteLine("安全に走査できました。");
}
}
安全に走査できました。
反復変数は原則読み取り専用(値の変更が反映されないケース)
foreach (var x in list)
の x
に新しい値を代入しても、コレクションの要素は変わりません。
値型では特に混乱の元です。
using System;
class Program
{
static void Main()
{
int[] a = { 1, 2, 3 };
foreach (var x in a)
{
// x++ はローカル変数 x を変えるだけ。配列 a は変わらない。
// x++ を実行しても a の中身は不変
}
Console.WriteLine(string.Join(", ", a)); // 1, 2, 3
// 参照型の内部を書き換えるのは有効(オブジェクト自体を変更)
var people = new Person[]
{
new Person { Name = "Alice", Age = 20 },
new Person { Name = "Bob", Age = 30 }
};
foreach (var p in people)
{
p.Age++; // 参照先オブジェクトのフィールドを変更
}
foreach (var p in people)
{
Console.WriteLine($"{p.Name}: {p.Age}");
}
}
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
1, 2, 3
Alice: 21
Bob: 31
IEnumerable と foreach の仕組み(GetEnumerator と Current/MoveNext)
foreach
は内部的に次の「列挙子パターン」を使います。
- コレクションは
GetEnumerator()
を提供 - 列挙子は
MoveNext()
とCurrent
を提供 - ループごとに
MoveNext()
を呼び、Current
を読み取る
これを手で書くと以下のようになります。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
IEnumerable<int> source = new List<int> { 10, 20, 30 };
using var enumerator = source.GetEnumerator(); // 取得
while (enumerator.MoveNext()) // 次へ
{
int current = enumerator.Current; // 現在値
Console.WriteLine(current);
}
}
}
10
20
30
列挙可能な型は IEnumerable
/IEnumerable<T>
を実装するのが一般的で、C# の yield return
を使うと列挙子を簡単に実装できます。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (var n in GenerateOdds(5))
{
Console.WriteLine(n);
}
}
// 1,3,5,7,9 を生成
static IEnumerable<int> GenerateOdds(int count)
{
int value = 1;
for (int i = 0; i < count; i++)
{
yield return value; // 列挙ごとに 1 つ返す
value += 2;
}
}
}
1
3
5
7
9
パフォーマンスと実践的なコツ
foreach と LINQ の使い分け(可読性 vs. パフォーマンス)
LINQ は宣言的で読みやすく、Where
や Select
を組み合わせた複雑な処理を簡潔に表現できます。
一方で、イテレーターやデリゲートのオーバーヘッドがあり、ホットパス(頻繁に呼ばれる内側ループ)では foreach
のほうが速いことが多いです。
原則として以下を目安にします。
- 可読性重視、性能要件が緩い箇所: LINQ を積極活用
- 性能重視の内側ループ、大量データの処理:
foreach
やfor
を検討
また、LINQ の終端操作(ToList
/ToArray
/Count
/Any
など)で不要なアロケーションが生じることがあります。
必要最小限にとどめるとよいです。
大規模データの最適化ポイント(構造体/参照型のコスト)
- 配列は
for
が最速級であることが多いです。List<T>
はforeach
でも非常に速く、列挙子は構造体(値型)でボックス化が発生しません。 - ただし、
IEnumerable<T>
として扱うと列挙子がインターフェース越しになり、値型列挙子がボックス化される場合があります。ホットパスでは具体型(List<T>
など)で扱うことで回避できます。 - 参照型の要素はアクセス自体は軽いですが、要素が大量で GC プレッシャーになる場合は、アロケーション回数を減らす工夫(プリアロケーション、プールの利用)を検討します。
- ループ内でラムダを生成するとクロージャ(キャプチャ)が発生してヒープ確保につながることがあります。ホットパスでは避けると有利です。
方法 (Method) | パフォーマンス (Performance) | 特徴 (Notes) |
---|---|---|
配列 × for | 最速候補 | 組み込み型配列に対して、インデックスアクセスが高速。最もパフォーマンスが求められる場合に適しています。 |
List × foreach | 実用上十分に高速 | List<T> などのコレクションに対して、シンプルで読みやすい記述。多くの場面で十分な速度が得られます。 |
IEnumerable × LINQ 連鎖 | 可読性は高いが、ホットパスでは注意 | 宣言的な記述でコードの意図が明確になります。遅延実行のため、パフォーマンスが重要となる「ホットパス」(頻繁に実行される処理)では注意が必要です。 |
参考:await foreach(C# 8 の非同期ストリーム)への入口
C# 8 以降は IAsyncEnumerable<T>
と await foreach
により、非同期に到着するデータを逐次処理できます。
例えば「ネットワークから届くメッセージを都度処理」する場面で有効です。
// .NET Core 3.0+ / C# 8+ が必要
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await foreach (var n in CountAsync(3))
{
Console.WriteLine($"Received: {n}");
}
Console.WriteLine("Done");
}
// 0,1,2 を 500ms 間隔で非同期に流す
static async IAsyncEnumerable<int> CountAsync(int count)
{
for (int i = 0; i < count; i++)
{
await Task.Delay(500); // 非同期待機
yield return i; // 値を 1 つ流す
}
}
}
Received: 0
Received: 1
Received: 2
Done
この仕組みにより、ストリーム状のデータを「溜め込まずに」順に処理でき、リアクティブなアプリケーションに適しています。
まとめ
foreach
は、コレクション全体を安全かつ簡潔に処理するための基本構文です。
配列・List・Dictionary といった主要コンテナで同じように使えるため、習得すればコードの見通しが大きく向上します。
制御構文(break/continue/return)の使いどころ、走査中の変更禁止というルール、null
・空コレクションの扱い、反復変数が読み取り専用である点などの基本を押さえることで、典型的なバグを防げます。
内部的な IEnumerable
/IEnumerator
の仕組みを理解すれば、LINQ や await foreach
にも自然に接続できます。
性能面では、配列には for
、List には foreach
を基本線とし、LINQ は可読性の向上に活用しつつ、ホットパスではオーバーヘッドに注意しましょう。
最初の一歩としては、まず foreach
で「全件処理」を迷いなく書けるようになることが上達への近道です。