- C#
C# foreach・forの使い方|インデックスとbreak/continue
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期連続の黒字決算。