Menu

C#のforeach:配列、List、Dictionary、インデックス、エラー

C#のforeachの動き方を解説します。配列、リスト、文字列、辞書(KeyValuePairと分解)のループ、インデックスの取得、ループ変数が読み取り専用である理由、「Collection was modified」のInvalidOperationExceptionとその対処、そしてforeachで使える型の条件を扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

foreach は、コレクションのすべての要素に対して順に1回ずつ本体を実行し、管理すべきインデックスはありません。

出力:

Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]

このループは、配列、List<T>、文字列(要素の型は char)、HashSet<T>、LINQのクエリなど、どんなシーケンスでも同じように動きます。宣言する型は var にでき、要素の型名が長いときによく使われます:foreach (var order in pendingOrders)。

foreachが裏で行っていること

foreach はコレクションに列挙子を要求し、それをたどります。List<int> の場合、コンパイラはループをおおよそ次のように変換します。

List<int>.Enumerator e = scores.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int score = e.Current;
        // loop body
    }
}
finally
{
    e.Dispose();
}

ここから3つのことが言えます。公開された GetEnumerator() メソッドを持つ型なら何でも使え、通常これは IEnumerable<T> を実装することで用意されます。本体が例外を投げたり break したりしても、列挙子は破棄されます。そして要素の変数は毎回 e.Current の新しいコピーなので、それに代入してもコレクションは変わりません。

GetEnumerator を持たない型は使えません。

error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'

型に IEnumerable<T> を実装するか、その型が持っているコレクションをループします(order ではなく order.Lines)。yield return で独自のシーケンスを書くこともでき、これはIEnumerableとyieldのページで扱います。

Dictionaryをループする

辞書の要素は、Key と Value プロパティを持つ KeyValuePair<TKey, TValue> の値です。

出力:

apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
 30 kiwis
 12 apples
  0 pears

Dictionary は列挙の順序を何も保証しません。実際にはエントリを削除するまでは挿入順になりますが、順序に依存するコードは、最後のループが OrderByDescending で行っているように明示的に並べ替えるか、SortedDictionary を使います。

.NET Core 2.0以降(および現代のすべての.NET)では KeyValuePair に Deconstruct メソッドがあるので、ループのヘッダーで組を直接2つの変数に分けられます。

// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
    Console.WriteLine($"{fruit}: {count}");
}

これが最も短い形で、現在のコードでよく目にする書き方です。KeyValuePair に Deconstruct がない.NET Frameworkではコンパイルできないので、そこでは item.Key と item.Value を使います。

インデックスを取得する

foreach 自体にはインデックスがありません。取得する一般的な方法は2つで、自分で管理するカウンターか、各要素とともにインデックスを渡す LINQ の Select のオーバーロードです。

出力:

1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar

カウンターが最も軽く、最も明確です。インデックスを使って podium[i - 1] を読んだり podium[i] = ... と書いたりしているなら、まさにそのために作られている for ループに切り替えます。

ループ変数は読み取り専用

反復変数に代入することはできません。

foreach (string name in names)
{
    name = name.Trim();
    // error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}

仮に許されたとしても、変わるのはローカルなコピーだけです。すべての要素を変換するには、新しいシーケンスを作る(names.Select(n => n.Trim()).ToArray())か、names[i] = names[i].Trim() と書き込む for ループを使います。

できることは、要素がクラスか構造体かによって変わります。クラスの場合、変数は参照を保持しているので、プロパティを変更するとコレクション内のオブジェクトが変わります。

出力:

Order 1: shipped
Order 2: shipped

Order が struct なら、同じ代入はCS1654(Cannot modify members of 'order' because it is a 'foreach iteration variable')で失敗します。変数は値への参照ではなく、値のコピーだからです。

コレクションの変更:InvalidOperationException

foreach がたどっている最中に List や Dictionary の要素を追加または削除すると、列挙子が壊れます。次の MoveNext が「Collection was modified; enumeration operation may not execute.」というメッセージで InvalidOperationException を投げます。

出力:

InvalidOperationException: collection was modified
milk, bread
milk, bread

対処法を、簡単なものから柔軟なものの順に挙げます。

  • List<T> の RemoveAll(predicate) は、一致するものを1回の走査ですべて削除します。
  • ToList() や ToArray() でコピーをループし、元のコレクションを自由に変更します。
  • 先に集めて後で適用する:ループ中に削除するキーを別のリストに集め、ループの後で削除します。Dictionary ではこれが一般的な形です。
  • RemoveAt(i) を使う逆順の for ループ。

Dictionary には知っておくべき例外が1つあります。.NET Core 3.0以降、foreach で辞書をたどっている最中に既存のキーの値を上書きすること(stock[key] = 0)やエントリを削除することでは例外が投げられなくなりましたが、新しいキーの追加では今も投げられます。.NET Frameworkでは3つとも例外になるので、先にキーを集める方法がどこでも動く書き方です。

break、continue、nullのコレクション

break と continue は他のすべてのループと同じように動きます。

出力:

reading 21
reading 23
no tags, no crash

null のコレクションをループすると、GetEnumerator が呼ばれたときに NullReferenceException が投げられます。?? Enumerable.Empty<T>() はnullかもしれないシーケンスを空のシーケンスに変えます。さらによいのは、自分のメソッドから null ではなく空のコレクションを返すことです。

foreachとList.ForEachの違い

List<T> には ForEach(Action<T>) メソッドがあります:names.ForEach(n => Console.WriteLine(n));。これはループ文と同じではありません。本体がラムダなので、break、continue、yield は使えず、return は現在の要素の呼び出しを終えるだけです。また List<T> にしかなく、配列や他のシーケンスにはありません。foreach 文を優先し、ForEach はメソッドとしてすでにある1行の処理に限って使います。

よくある質問

C#のforeachでDictionaryをループするには?

各要素は KeyValuePair<TKey, TValue> です:foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");。.NET Core 2.0以降では分解できます:foreach (var (name, price) in prices)。キーだけ、または値だけをループするには prices.Keys か prices.Values を使います。

C#のforeachでインデックスを取得するには?

foreach には組み込みのインデックスはありません。カウンターを持つ(ループの前に int i = 0;、本体の最後に i++)か、LINQで各要素をインデックスとともに射影します:foreach (var item in names.Select((name, index) => new { name, index }))。書き戻しや隣の要素との比較にインデックスが必要なら、for ループのほうが向いています。

「Collection was modified; enumeration operation may not execute」とはどういう意味ですか?

foreach が列挙している最中に、List、Dictionary などのコレクションに要素を追加または削除したということです。列挙子は変更を検出し、次のステップで InvalidOperationException を投げます。コピーをループする(foreach (var x in list.ToList()))、変更を集めてループの後に適用する、list.RemoveAll(...) を使う、あるいは逆順の for ループを使います。

C#でforeachの変数に代入できないのはなぜですか?

反復変数は読み取り専用です。foreach (string name in names) の中の name = name.Trim(); はエラーCS1656になります。代入できたとしてもコレクションは変わりません。要素を置き換えるには、インデックスを使う for ループか、Select で新しいコレクションを作ります。クラスのオブジェクトのプロパティを変更すること(order.Status = ...)は許されます。

C#のforeachループからbreakで抜けられますか?

抜けられます。for や while とまったく同じく、break はループをすぐに終わらせ、continue は次の要素に進みます。return はメソッド全体から抜けます。List<T>.ForEach メソッドは別物で、本体がラムダなので break と continue は使えません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める