C# の foreach は「コレクションの要素を先頭から1つずつ取り出す」ループ、for は「カウンタを自分で動かす」ループです。 全要素を順番に読むだけなら foreach、インデックスが必要なとき・逆順や1つ飛ばしで回すとき・ループ中に要素を削除するときは for が向いています。

この記事では、基本の書き方に加えて、インデックスを取得する4つの書き方の比較、foreach の途中で Remove したときに出る InvalidOperationException の再現と回避、配列・List・Dictionary・Span での使い分け、ネストしたループからの抜け方、反復変数に代入できない理由 までを扱います。コードはすべて .NET SDK 8.0.131(C# 12)で実行し、出力をそのまま載せています。.NET 9 以降の機能は動かせなかったため、その箇所には「未実行」と明記しました。

foreach と for の早見表

やりたいこと 選ぶ書き方 コード
全要素を順番に読む foreach foreach (var x in items) { }
インデックスも使う for for (int i = 0; i < items.Length; i++) { }
foreach でインデックスも欲しい Select またはカウンタ foreach (var (x, i) in items.Select((x, i) => (x, i))) { }
.NET 9 以降でインデックスも欲しい Index() foreach (var (i, x) in items.Index()) { }
逆順に回す for(逆順) for (int i = items.Length - 1; i >= 0; i--) { }
N個おきに回す for for (int i = 0; i < n; i += 3) { }
決まった回数だけ繰り返す for または Range foreach (var i in Enumerable.Range(1, 3)) { }
ループを途中で終える break if (x > 4) break;
その回だけ飛ばす continue if (x % 2 == 0) continue;
条件に合う要素を削除する RemoveAll list.RemoveAll(x => x % 2 == 0);
Dictionary を回す foreach と分解 foreach (var (key, value) in dict) { }
要素をその場で書き換える for または Span foreach (ref int x in span) x *= 10;
比べる点 foreach for
インデックス 持たない カウンタとして持つ
範囲の指定 不要(全要素) 開始・終了・増分を自分で書く
使える対象 配列・List・Dictionary・文字列・Span など列挙できるもの全般 インデックスでアクセスできるもの
要素への代入 反復変数には代入できない items[i] = 値; と書ける
ループ中の追加・削除(List) InvalidOperationException になる 逆順なら安全に削除できる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# foreach・forの使い方|インデックスとbreak/continue」

foreach と for の基本の書き方

同じ配列を、for と foreach でそれぞれ回します。.NET 6 以降のコンソールアプリ(dotnet new console)なら、Program.cs にそのまま貼って dotnet run で動きます。

string[] items = { "ペン", "ノート", "消しゴム" };

Console.WriteLine("--- for");
for (int i = 0; i < items.Length; i++)
{
    Console.WriteLine($"{i}: {items[i]}");
}

Console.WriteLine("--- foreach");
foreach (var item in items)
{
    Console.WriteLine(item);
}
--- for
0: ペン
1: ノート
2: 消しゴム
--- foreach
ペン
ノート
消しゴム

for の丸かっこの中は、初期化(int i = 0)・続ける条件(i < items.Length)・1回ごとの更新(i++) の3つをセミコロンで区切ります。配列の要素数は Length、List の要素数は Count です。

foreach は foreach (型 変数 in コレクション) の形で、取り出した要素が毎回その変数に入ります。 型は var と書けばコンパイラが推論します。範囲を書かないので、終了条件の書き間違い(< と <= の取り違えなど)が起きないのが利点です。

逆順・N個おき・決まった回数で回す

string[] items = { "ペン", "ノート", "消しゴム" };

// 逆順(for)
for (int i = items.Length - 1; i >= 0; i--) Console.Write(items[i] + " ");
Console.WriteLine();

// 逆順(foreach と Reverse)
foreach (var x in items.Reverse()) Console.Write(x + " ");
Console.WriteLine();

// 3つおき
for (int i = 0; i < 10; i += 3) Console.Write(i + " ");
Console.WriteLine();

// 1から3まで
foreach (var i in Enumerable.Range(1, 3)) Console.Write(i + " ");
Console.WriteLine();

// 2番目以降だけ(範囲演算子、C# 8.0 以降)
foreach (var x in items[1..]) Console.Write(x + " ");
Console.WriteLine();
消しゴム ノート ペン 
消しゴム ノート ペン 
0 3 6 9 
1 2 3 
ノート 消しゴム 

Enumerable.Range(1, 3) の第2引数は「終わりの値」ではなく 「個数」 です。Range(5, 3) なら 5、6、7 になります。

break と continue でループを制御する

break はループそのものを終了し、continue はその回の残りを飛ばして次の回へ進みます。 for・foreach・while のどれでも同じように使えます。

foreach (var x in new[] { 1, 2, 3, 4, 5, 6 })
{
    if (x % 2 == 0) continue;   // 偶数は飛ばす
    if (x > 4) break;           // 4を超えたら終了
    Console.WriteLine(x);
}
1
3

処理の流れを追うと次のようになります。

x x % 2 == 0 x > 4 起きること
1 false false 1 を表示
2 true — continue で次へ
3 false false 3 を表示
4 true — continue で次へ
5 false true break でループ終了
6 — — 実行されない
キーワード 動き 抜ける範囲
break ループを終了する いちばん内側のループ1つ
continue その回を飛ばして次の回へ いちばん内側のループ1つ
return メソッドごと終了する すべてのループとメソッド

for で continue を使った場合、更新の式(i++)は実行されてから次の回の条件判定に進みます。 while で continue を使うときは、カウンタの更新を continue より前に書いておくと、同じ回を繰り返さずに済みます。

インデックスを取得する4つの書き方

foreach にはインデックスがありません。「何番目か」も使いたいときの書き方は4つあります。

1. for で書く

string[] items = { "ペン", "ノート", "消しゴム" };
for (int i = 0; i < items.Length; i++)
{
    Console.WriteLine($"{i}: {items[i]}");
}

2. Select((x, i)) とタプルの分解

LINQ の Select には、要素とインデックスの両方を受け取る書き方 があります。タプルにして返し、foreach の側で分解します。

string[] items = { "ペン", "ノート", "消しゴム" };
foreach (var (item, i) in items.Select((x, i) => (x, i)))
{
    Console.WriteLine($"{i}: {item}");
}

3. 手動カウンタ

string[] items = { "ペン", "ノート", "消しゴム" };
int n = 0;
foreach (var item in items)
{
    Console.WriteLine($"{n}: {item}");
    n++;
}

1〜3 の実行結果はどれも同じです。

0: ペン
1: ノート
2: 消しゴム

4. .NET 9 の Index()

.NET 9 で、LINQ に Index() メソッドが加わりました。インデックスと要素のタプルを返します。

// .NET 9 以降(今回の環境は .NET 8 のため未実行)
string[] items = { "ペン", "ノート", "消しゴム" };
foreach (var (index, item) in items.Index())
{
    Console.WriteLine($"{index}: {item}");
}

戻り値の型は IEnumerable<(int Index, TSource Item)> で、タプルの順番は「インデックスが先、要素が後」 です(Microsoft 公式の Enumerable.Index の API リファレンスで確認)。Select の例では自分で (x, i) と書いたので要素が先でしたが、Index() は逆になる点に気をつけます。

.NET 8 の環境で同じコードをビルドすると、次のエラーになりました(実行結果)。

error CS1061: 'List<string>' does not contain a definition for 'Index' and no accessible extension method 'Index' accepting a first argument of type 'List<string>' could be found (are you missing a using directive or an assembly reference?)

.NET 8 以前で同じ書き味にしたい場合は、次の拡張メソッドを1つ用意すると近い形で書けます。こちらは .NET 8 で実行して、1〜3 と同じ出力になることを確かめました。

string[] items = { "ペン", "ノート", "消しゴム" };
foreach (var (index, item) in items.WithIndex())
{
    Console.WriteLine($"{index}: {item}");
}

static class Ext
{
    public static IEnumerable<(int Index, T Item)> WithIndex<T>(this IEnumerable<T> source)
    {
        int i = 0;
        foreach (var x in source) yield return (i++, x);
    }
}

4つの書き方の比較表

書き方 使えるバージョン 使える対象 向いている場面
for C# 1.0 から 配列・List などインデックスでアクセスできるもの インデックスを使った計算、前後の要素との比較、書き換え
手動カウンタ C# 1.0 から 列挙できるもの全般 LINQ を使わずに済ませたい、条件に合ったものだけ数えたい
Select((x, i) => (x, i)) と分解 C# 7.0 から(タプルの分解)。Select 自体は .NET Framework 3.5 から 列挙できるもの全般 Where などの LINQ とつなげて書きたい
Index() .NET 9 から 列挙できるもの全般 .NET 9 以降でいちばん短く書きたい

配列や List なら for、IEnumerable しか受け取れないときは Select か Index() と考えると選びやすくなります。Where で絞り込んだ後に Select((x, i)) や Index() をつなげると、インデックスは「絞り込んだ後の並びで何番目か」になります。元の位置が必要なら、先にインデックスをつけてから絞り込みます。

foreach の途中で Remove するとエラーになる例

List を foreach で回しながら、同じ List から要素を削除すると例外になります。実際に再現します。

var list = new List<int> { 1, 2, 3, 4, 5, 6 };
try
{
    foreach (var x in list)
    {
        if (x % 2 == 0) list.Remove(x);
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine($"{e.GetType().Name}: {e.Message}");
}
Console.WriteLine(string.Join(",", list));
InvalidOperationException: Collection was modified; enumeration operation may not execute.
1,3,4,5,6

最初の偶数(2)は削除され、次の要素へ進もうとした時点で例外 が出ています。List は内部に「変更された回数」を持っていて、foreach は次の要素へ進むたびに、開始時点の回数と同じかを確認します。違っていれば「列挙の途中で中身が変わった」と判断して InvalidOperationException を投げます。要素を飛ばしたり二重に処理したりしないための仕組みです。

Remove だけでなく、次の操作でも同じ例外になりました(.NET 8 で実行)。

foreach の中での操作(List) 結果
list.Remove(x) InvalidOperationException
list.Add(値) InvalidOperationException
list[0] = 99(インデクサーで上書き) InvalidOperationException
配列で arr[0] = 99 例外なし(99,2,3 になる)
要素がクラスのとき c.X = 9(プロパティの変更) 例外なし(コレクション自体は変わらないため)

前から for で削除すると要素を飛ばす

for に書き換えれば例外は出ませんが、前から順に削除すると、削除した直後の要素を飛ばします。

var l0 = new List<int> { 1, 2, 2, 3, 4, 4 };
for (int i = 0; i < l0.Count; i++)
{
    if (l0[i] % 2 == 0) l0.RemoveAt(i);
}
Console.WriteLine(string.Join(",", l0));
1,2,3,4

偶数をすべて消したいのに、2 と 4 が1つずつ残りました。インデックス 1 の「2」を削除すると後ろの要素が1つ前に詰まり、次の「2」がインデックス 1 に来ます。ところが i は 2 に進むので、その要素は判定されません。

回避1:逆順の for で削除する

var l1 = new List<int> { 1, 2, 2, 3, 4, 4 };
for (int i = l1.Count - 1; i >= 0; i--)
{
    if (l1[i] % 2 == 0) l1.RemoveAt(i);
}
Console.WriteLine(string.Join(",", l1));
1,3

後ろから削除すると、詰まるのは「すでに判定が済んだ側」だけ なので、飛ばしが起きません。

回避2:RemoveAll で条件を渡す

var l2 = new List<int> { 1, 2, 2, 3, 4, 4 };
int removed = l2.RemoveAll(x => x % 2 == 0);
Console.WriteLine($"{removed}件削除 -> {string.Join(",", l2)}");
4件削除 -> 1,3

条件に合う要素をまとめて削除するなら RemoveAll がいちばん短く、削除した件数も戻り値で受け取れます。

回避3:ToList() でコピーを回す

var l3 = new List<int> { 1, 2, 2, 3, 4, 4 };
foreach (var x in l3.ToList())   // コピーを列挙する
{
    if (x % 2 == 0) l3.Remove(x);
}
Console.WriteLine(string.Join(",", l3));
1,3

ToList() で作ったコピーを列挙し、元の List を変更するので、列挙中のコレクションは変わりません。削除のほかに追加や別の処理も混ぜたいときに便利です。

3つの回避方法の選び方

方法 コードの短さ 追加のメモリ 向いている場面
RemoveAll(条件) いちばん短い 不要 条件で削除するだけ
逆順の for と RemoveAt やや長い 不要 削除の前後で別の処理もしたい、インデックスも使いたい
ToList() のコピーを foreach 短い 要素数ぶんのコピーが必要 削除と追加が混ざる、foreach の形を保ちたい
Where(条件).ToList() で新しい List を作る 短い 新しい List が必要 元の List を変えたくない

配列・List・Dictionary・Span での使い分け

配列と List

配列と List はどちらも for と foreach の両方が使えます。違いは要素数のプロパティ名(配列は Length、List は Count)です。2次元配列を foreach に渡すと、全要素が行の順に1つずつ取り出されます。

int[,] m = { { 1, 2 }, { 3, 4 } };
foreach (var v in m) Console.Write(v + " ");
Console.WriteLine();

for (int r = 0; r < m.GetLength(0); r++)
    for (int c = 0; c < m.GetLength(1); c++)
        Console.Write($"[{r},{c}]={m[r, c]} ");
Console.WriteLine();
1 2 3 4 
[0,0]=1 [0,1]=2 [1,0]=3 [1,1]=4 

行と列の位置が必要なら for、値だけ順に見られればよいなら foreach を選びます。

Dictionary

Dictionary は foreach で回します。要素は KeyValuePair<TKey, TValue> で、タプルのように (キー, 値) へ分解して受け取れます(.NET Core 2.0 以降)。

var stock = new Dictionary<string, int> { ["ペン"] = 10, ["ノート"] = 0, ["消しゴム"] = 3 };

foreach (var (name, count) in stock) Console.WriteLine($"{name} = {count}");

foreach (var key in stock.Keys) Console.Write(key + " ");
Console.WriteLine();
foreach (var v in stock.Values) Console.Write(v + " ");
Console.WriteLine();
ペン = 10
ノート = 0
消しゴム = 3
ペン ノート 消しゴム 
10 0 3 

Dictionary は、foreach 中の変更について List と少し動きが異なります。.NET 8 で次の3つを試しました。

// (1) 既存のキーの値を上書き
foreach (var key in stock.Keys) stock[key] += 1;
Console.WriteLine(string.Join(", ", stock.Select(kv => $"{kv.Key}={kv.Value}")));

// (2) 列挙中に Remove
foreach (var kv in stock) if (kv.Value == 1) stock.Remove(kv.Key);
Console.WriteLine(string.Join(", ", stock.Select(kv => $"{kv.Key}={kv.Value}")));

// (3) 列挙中に新しいキーを追加
try { foreach (var kv in stock) stock["新規" + kv.Key] = 0; }
catch (InvalidOperationException e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }
ペン=11, ノート=1, 消しゴム=4
ペン=11, 消しゴム=4
InvalidOperationException: Collection was modified; enumeration operation may not execute.
foreach の中での操作(Dictionary、.NET 8 で実行) 結果
既存のキーの値を上書き 例外なし
Remove(キー) 例外なし
新しいキーの追加 InvalidOperationException

既存キーの上書きと Remove が列挙中でも通るのは .NET Core 3.0 以降の動き で、.NET Framework では例外になります(.NET Framework での動作は未実行。一般に知られた仕様です)。どの環境でも同じ結果にしたいときは、stock.Keys.ToList() のコピーを回す書き方にしておくと安心です。なお、Dictionary の列挙順は仕様として保証されていないので、順番に意味を持たせたいときはキーで並べ替えてから回します。

Span

Span は、配列や List の一部を「コピーせずに」指し示す型です。foreach に ref をつけると、要素をその場で書き換えられます(ref つきの反復変数は C# 7.3 以降)。

using System.Runtime.InteropServices;

int[] nums = { 1, 2, 3, 4, 5 };
Span<int> span = nums.AsSpan(1, 3);        // インデックス1から3個
foreach (ref int x in span) x *= 10;
Console.WriteLine(string.Join(",", nums));

var lst = new List<int> { 1, 2, 3 };
foreach (ref int x in CollectionsMarshal.AsSpan(lst)) x += 100;
Console.WriteLine(string.Join(",", lst));

ReadOnlySpan<char> chars = "abc";
foreach (char c in chars) Console.Write(c + "|");
Console.WriteLine();
1,20,30,40,5
101,102,103
a|b|c|

元の配列 nums の2〜4番目だけが10倍になっています。CollectionsMarshal.AsSpan(.NET 5 以降)は List の内部の配列を Span として取り出すメソッドで、Span を使っている間は List に要素を追加・削除しないことが前提です。

コレクション別の選び方

対象 おすすめの回し方 補足
配列 読むだけなら foreach、位置が必要なら for 要素数は Length
List 読むだけなら foreach、削除するなら RemoveAll か逆順 for 要素数は Count
Dictionary foreach (var (key, value) in dict) 列挙中の新規追加は例外
文字列 foreach (char c in text) 1文字ずつ取り出す
Span foreach (ref T x in span) その場で書き換えられる
IEnumerable<T> foreach インデックスは Select か Index() で

参考:1,000万要素を合計したときの時間

for と foreach の速さが気になる場合の参考として、1,000万個の int を合計する時間を測りました(.NET 8、Release ビルド、2回目の計測値)。

回し方 時間
配列を for 29.2 ms
配列を foreach 28.2 ms
List を for 139.1 ms
List を foreach 79.3 ms
List を Span にして foreach 17.0 ms
List を IEnumerable<int> 型として foreach 346.9 ms

1回の簡易計測なので、数値は環境や実行のたびに変わります。読み取れる傾向は、配列では for と foreach にほぼ差がない こと、同じ List でもインターフェース型(IEnumerable<int>)として回すと時間が増えることです。ふだんのコードでは読みやすさで選び、計測して本当に時間がかかっている箇所だけ Span などを検討すれば十分です。

ネストしたループから抜ける方法

break で抜けられるのは、いちばん内側のループ1つだけ です。二重ループの内側から全体を抜けたいときの書き方を4つ紹介します。3行3列のデータから 5 を探す例で比べます。

int[][] grid = { new[] { 1, 2, 3 }, new[] { 4, 5, 6 }, new[] { 7, 8, 9 } };

方法1:フラグ変数を使う

bool found = false;
foreach (var row in grid)
{
    foreach (var v in row)
    {
        if (v == 5) { found = true; break; }
        Console.Write(v + " ");
    }
    if (found) break;
}
Console.WriteLine("\nfound=" + found);
1 2 3 4 
found=True

方法2:メソッドに切り出して return する

Console.WriteLine(Find(grid, 5));

static string Find(int[][] g, int target)
{
    for (int r = 0; r < g.Length; r++)
        for (int c = 0; c < g[r].Length; c++)
            if (g[r][c] == target) return $"({r},{c})";
    return "なし";
}
(1,1)

方法3:goto でラベルへ移る

foreach (var row in grid)
    foreach (var v in row)
    {
        if (v == 5) goto done;
        Console.Write(v + " ");
    }
done:
Console.WriteLine("\ndone");
1 2 3 4 
done

方法4:LINQ で1つの列にしてから探す

var hit = grid
    .SelectMany((row, r) => row.Select((v, c) => (r, c, v)))
    .FirstOrDefault(t => t.v == 5);
Console.WriteLine(hit);
(1, 1, 5)
方法 読みやすさ 向いている場面
メソッドに切り出して return 高い 「探す」「判定する」のように結果を返す処理。まずこれを検討
フラグ変数 ふつう ループの後にも処理が続く、メソッドに分けにくい
goto 好みが分かれる 多重ループを1か所で抜けたい。使うならラベルはループの直後に置く
LINQ(SelectMany と FirstOrDefault) 高い(慣れが必要) 条件に合う最初の1件を探すだけ

C# には、Java のような「ラベルつき break」はありません。探す処理をメソッドに切り出して return で返す形 が、ループの目的も名前で伝わるのでおすすめです。

反復変数に代入できない理由

foreach の変数に代入しようとすると、コンパイルエラーになります。

var names = new List<string> { "a", "b" };
foreach (var n in names)
{
    n = n.ToUpper();   // エラー
}
error CS1656: Cannot assign to 'n' because it is a 'foreach iteration variable'

理由は、foreach がコンパイラによって次のような形に展開されるためです(展開のイメージ)。

var e = names.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        var n = e.Current;   // 要素を読み取り専用の変数に受け取る
        // ループの本体
    }
}
finally
{
    e.Dispose();
}

反復変数は Current から受け取った値を入れた、その回だけの変数 です。ここへ代入できたとしても、変わるのはその変数だけで、コレクションの中身は変わりません。「書き換えたつもりが反映されていない」という誤解を防ぐために、C# は代入そのものをコンパイルエラーにしています。

要素を書き換えたいときの書き方

コレクションの中身を変えたいときは、for でインデックスを指定して代入します。

var names = new List<string> { "a", "b" };
for (int i = 0; i < names.Count; i++)
{
    names[i] = names[i].ToUpper();
}
// または新しい List を作る
var upper = names.Select(n => n.ToUpper()).ToList();

構造体のメンバーも変更できない

要素が構造体(struct)のときは、メンバーの変更もコンパイルエラーになります。

var pts = new List<P> { new P { X = 1 }, new P { X = 2 } };
foreach (var p in pts) { p.X = 9; }   // エラー

struct P { public int X; }
error CS1654: Cannot modify members of 'p' because it is a 'foreach iteration variable'

構造体は値型なので、反復変数に入っているのは要素のコピーです。コピーを書き換えても元の List には反映されないため、こちらもエラーで知らせてくれます。for で「取り出す・変更する・戻す」の順に書くと反映されます。

for (int i = 0; i < pts.Count; i++)
{
    var p = pts[i];
    p.X *= 10;
    pts[i] = p;
}
Console.WriteLine(string.Join(",", pts.Select(p => p.X)));
10,20

一方、要素がクラス(参照型)なら、foreach の中でプロパティやフィールドを変更できます。反復変数が指している先は、コレクションの要素と同じインスタンスだからです。

var cs = new List<C> { new C { X = 1 } };
foreach (var c in cs) c.X = 9;
Console.WriteLine(cs[0].X);

class C { public int X; }
9
要素の種類 反復変数への代入 メンバーの変更
クラス(参照型) コンパイルエラー(CS1656) できる(元の要素に反映される)
構造体(値型) コンパイルエラー(CS1656) コンパイルエラー(CS1654)
Span を ref つきで回したとき できる(元の要素に反映される) できる

ラムダ式に取り込むときの違い

反復変数が「その回だけの変数」であることは、ラムダ式に取り込んだときの結果にも表れます。

var acts = new List<Action>();
foreach (var x in new[] { 1, 2, 3 }) acts.Add(() => Console.Write(x + " "));
foreach (var a in acts) a();
Console.WriteLine();

var acts2 = new List<Action>();
for (int i = 1; i <= 3; i++) acts2.Add(() => Console.Write(i + " "));
foreach (var a in acts2) a();
Console.WriteLine();
1 2 3 
4 4 4 

foreach は回ごとに新しい変数になるので 1 2 3、for の i はループ全体で1つの変数なので、後から実行するとすべて最後の値(4) になります。for でその回の値を取り込みたいときは、ループの中で int n = i; と別の変数に写してからラムダ式で使います。

つまずきやすい点と直し方

起きること 原因 こうするとよい
Collection was modified の例外 foreach 中に同じ List を追加・削除・上書きした RemoveAll、逆順 for、ToList() のコピーを回す
削除したはずの要素が残る 前から for で RemoveAt して要素を飛ばした 逆順の for にする
IndexOutOfRangeException 条件を i <= items.Length と書いた i < items.Length にする
CS1656(代入できない) 反復変数に代入した for で items[i] = 値; と書く
CS1654(メンバーを変更できない) 構造体の要素を foreach で変更した for で取り出して戻す、または Span と ref
CS1061(Index がない) .NET 8 以前で Index() を使った Select((x, i) => (x, i)) か拡張メソッド
for のラムダ式がすべて同じ値になる i がループ全体で1つの変数 ループ内で別の変数に写す
NullReferenceException null のコレクションを foreach に渡した foreach (var x in list ?? Enumerable.Empty<int>())
break で外側のループが止まらない break は内側の1つだけを抜ける メソッドに切り出して return、またはフラグ

null のコレクションを foreach に渡した場合の実行結果は次のとおりです。

List<int>? nul = null;
try { foreach (var x in nul!) { } }
catch (NullReferenceException e) { Console.WriteLine(e.GetType().Name + ": " + e.Message); }

foreach (var x in nul ?? Enumerable.Empty<int>()) { }
Console.WriteLine("ok");
NullReferenceException: Object reference not set to an instance of an object.
ok

あわせて使える便利な書き方

string[] items = { "ペン", "ノート", "消しゴム" };

// 2つの列を同時に回す(Zip)
foreach (var (a, b) in items.Zip(new[] { 120, 200, 80 })) Console.Write($"{a}:{b} ");
Console.WriteLine();

// 3個ずつに分けて回す(Chunk、.NET 6 以降)
foreach (var chunk in Enumerable.Range(1, 7).Chunk(3)) Console.Write($"[{string.Join(",", chunk)}] ");
Console.WriteLine();

// List の ForEach メソッド
new List<int> { 1, 2, 3 }.ForEach(x => Console.Write(x + " "));
Console.WriteLine();
ペン:120 ノート:200 消しゴム:80 
[1,2,3] [4,5,6] [7] 
1 2 3 

List<T>.ForEach はメソッドなので、中で break や continue は使えません。途中で抜けたい処理は foreach 文で書きます。

よくある質問

Q. foreach と for はどちらを使えばよいですか? A. 全要素を順番に読むだけなら foreach が向いています。範囲の書き間違いが起きず、意図も伝わりやすいためです。インデックスを使う、逆順や N 個おきに回す、要素を書き換える・削除する、といった場合は for を選びます。

Q. foreach でインデックスを取得するいちばん簡単な方法は? A. .NET 9 以降なら foreach (var (i, x) in items.Index()) です。.NET 8 以前なら items.Select((x, i) => (x, i)) をタプルで分解するか、ループの外にカウンタ変数を置きます。配列や List なら、for に書き換えるのもわかりやすい方法です。

Q. foreach の中で要素を削除したいときはどうすればよいですか? A. 条件で削除するなら list.RemoveAll(x => 条件) が最短です。削除の前後に別の処理を入れたいときは逆順の for、foreach の形を保ちたいときは list.ToList() のコピーを回して元の List から Remove します。

Q. continue と break の違いは? A. continue はその回の残りを飛ばして次の回に進み、break はループ自体を終了します。どちらもいちばん内側のループにだけ効きます。

まとめ

  • 全要素を読むなら foreach、インデックス・逆順・書き換え・削除が関わるなら for を選ぶ
  • インデックスの取得は、for・Select((x, i))・手動カウンタ・.NET 9 の Index() の4通り。Index() のタプルは「インデックス、要素」の順
  • foreach 中に List を Add・Remove・上書きすると InvalidOperationException(Collection was modified)になる
  • 削除は RemoveAll、逆順の for、ToList() のコピー のどれかで安全に書ける。前から for で削除すると要素を飛ばす
  • Dictionary は foreach (var (key, value) in dict)、Span は foreach (ref T x in span) でその場の書き換えができる
  • break が抜けるのは内側のループ1つ。多重ループはメソッドに切り出して return するのが読みやすい
  • 反復変数はその回だけの読み取り専用の変数 なので代入できない(CS1656)。構造体のメンバー変更は CS1654 になる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# foreach・forの使い方|インデックスとbreak/continue」

株式会社RJCでは、C#のループ処理の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?