【C#】foreachとforの違いをわかりやすく比較:使い分けポイントとコーディング例
foreach
は要素を順に取り出すときに安全で読みやすいが、ループ中にコレクションを変更できず要素を書き換えにくいです。
for
はインデックス操作や途中スキップ、逆順など柔軟で要素を直接更新できる一方、境界管理を誤るリスクや読みやすさが下がりがちです。
速度差は大きくなく用途に合わせ選ぶのが最適です。
forとforeachの基本構文
C#で繰り返し処理を行う際に使われる代表的な構文がfor
文とforeach
文です。
どちらもループ処理を実現しますが、書き方や使い方に違いがあります。
ここではまず、それぞれの基本的な構文の形を見ていきましょう。
for構文の形
for
文は、ループの開始条件、終了条件、そしてループごとに実行される処理(通常はインクリメントやデクリメント)を明示的に指定する構文です。
主にインデックスを使って配列やリストの要素にアクセスする場合に使われます。
基本的な構文は以下のようになります。
for (初期化式; 条件式; 反復処理)
{
// 繰り返し実行する処理
}
具体的には、例えば配列の要素を順番に処理する場合は次のように書きます。
for (int i = 0; i < array.Length; i++)
{
// array[i]を使った処理
}
ここで、
int i = 0
はループ変数の初期化です。0からスタートしますi < array.Length
はループの継続条件で、配列の長さより小さい間ループを続けますi++
はループ変数を1ずつ増やす処理です
このように、for
文はループの制御を細かく指定できるため、インデックスを使った処理や途中でループの範囲を変えたい場合に便利です。
foreach構文の形
一方、foreach
文はコレクションの要素を順番に取り出して処理するための構文です。
インデックスを意識せずに書けるため、コードがシンプルで読みやすくなります。
基本的な構文は以下の通りです。
foreach (型 変数名 in コレクション)
{
// 変数名を使った処理
}
例えば、配列の全要素を順に処理する場合は次のように書きます。
foreach (var item in array)
{
// itemを使った処理
}
ここで、
var item
はコレクションの要素を受け取る変数です。型を明示的に書くこともできますin array
はループ対象のコレクションを指定します
foreach
文はコレクションの要素を1つずつ取り出して処理するため、インデックスを使う必要がない場合や、全要素を順番に処理したい場合に適しています。
コード例で見る違い
ここまでの説明を踏まえて、for
文とforeach
文の違いを具体的なコード例で比較してみましょう。
以下のサンプルは、整数の配列の全要素をコンソールに表示する処理です。
using System;
class Program
{
static void Main()
{
int[] numbers = { 10, 20, 30, 40, 50 };
// for文を使った例
Console.WriteLine("for文の出力:");
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"インデックス {i} の値: {numbers[i]}");
}
// foreach文を使った例
Console.WriteLine("\nforeach文の出力:");
foreach (int number in numbers)
{
Console.WriteLine($"値: {number}");
}
}
}
for文の出力:
インデックス 0 の値: 10
インデックス 1 の値: 20
インデックス 2 の値: 30
インデックス 3 の値: 40
インデックス 4 の値: 50
foreach文の出力:
値: 10
値: 20
値: 30
値: 40
値: 50
この例では、
for
文ではループ変数i
を使って配列のインデックスを明示的に指定し、要素を取得しています。インデックスを表示しているため、どの位置の要素かがわかりやすいですforeach
文ではインデックスを意識せずに、配列の要素を順番にnumber
変数に代入して処理しています。コードがシンプルで読みやすいのが特徴です
このように、for
文はインデックスを使った細かい制御が必要な場合に向いており、foreach
文は全要素を順に処理したい場合に適しています。
イテレーション対象別の適用
配列
配列はC#で最も基本的なコレクションの一つで、固定長の連続したメモリ領域に要素が格納されています。
for
文とforeach
文の両方で簡単にイテレーションできますが、それぞれの特徴を理解して使い分けることが重要です。
for
文は配列のインデックスを直接指定できるため、特定の範囲だけ処理したり、逆順に処理したりする場合に便利です。
例えば、配列の後ろから前に向かって処理したいときはfor
文が適しています。
一方、foreach
文は配列の全要素を順番に処理するのに向いています。
インデックスを意識せずに書けるため、コードがシンプルで読みやすくなります。
以下は配列を使ったfor
文とforeach
文の例です。
using System;
class Program
{
static void Main()
{
string[] fruits = { "りんご", "みかん", "バナナ", "ぶどう" };
// for文で配列を処理(逆順)
Console.WriteLine("for文(逆順):");
for (int i = fruits.Length - 1; i >= 0; i--)
{
Console.WriteLine($"インデックス {i}: {fruits[i]}");
}
// foreach文で配列を処理(順方向)
Console.WriteLine("\nforeach文(順方向):");
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}
}
}
for文(逆順):
インデックス 3: ぶどう
インデックス 2: バナナ
インデックス 1: みかん
インデックス 0: りんご
foreach文(順方向):
りんご
みかん
バナナ
ぶどう
このように、配列ではfor
文でインデックスを活用した細かい制御が可能で、foreach
文はシンプルに全要素を処理したい場合に適しています。
List<T>
List<T>
は可変長のコレクションで、配列よりも柔軟に要素の追加や削除が可能です。
List<T>
はインデクサーを持っているため、for
文でインデックスを使ったアクセスが簡単にできます。
for
文は特定の範囲だけ処理したり、要素の位置を意識した処理を行いたい場合に便利です。
また、ループ中に要素の追加や削除を行う場合は、for
文の方が安全に操作できます。
一方、foreach
文は全要素を順番に処理するのに向いていますが、ループ中にコレクションを変更すると例外が発生するため注意が必要です。
以下はList<string>
を使った例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> colors = new List<string> { "赤", "青", "緑", "黄" };
// for文でListを処理(インデックス利用)
Console.WriteLine("for文:");
for (int i = 0; i < colors.Count; i++)
{
Console.WriteLine($"インデックス {i}: {colors[i]}");
}
// foreach文でListを処理
Console.WriteLine("\nforeach文:");
foreach (var color in colors)
{
Console.WriteLine(color);
}
}
}
for文:
インデックス 0: 赤
インデックス 1: 青
インデックス 2: 緑
インデックス 3: 黄
foreach文:
赤
青
緑
黄
List<T>
ではfor
文とforeach
文のどちらも使いやすいですが、要素の追加・削除を伴う処理やインデックスを使った操作が必要な場合はfor
文を選ぶとよいでしょう。
Dictionary<TKey, TValue>
Dictionary<TKey, TValue>
はキーと値のペアを管理するコレクションで、キーを使って高速に値を取得できます。
for
文はインデックスを使ったアクセスができないため、Dictionary
のイテレーションには基本的にforeach
文を使います。
foreach
文ではKeyValuePair<TKey, TValue>
型の要素を順に取り出せるため、キーと値の両方を簡単に扱えます。
以下はDictionary<string, int>
をforeach
文で処理する例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "太郎", 80 },
{ "花子", 90 },
{ "次郎", 75 }
};
// foreach文でDictionaryを処理
foreach (var kvp in scores)
{
Console.WriteLine($"名前: {kvp.Key}, 点数: {kvp.Value}");
}
}
}
名前: 太郎, 点数: 80
名前: 花子, 点数: 90
名前: 次郎, 点数: 75
Dictionary
は順序が保証されないため、順番を意識した処理が必要な場合は別途ソートや変換が必要です。
for
文は使えないため、foreach
文が基本となります。
IEnumerable<T>全般
IEnumerable<T>
はC#のコレクションの基本インターフェースで、多くのコレクションがこれを実装しています。
foreach
文はIEnumerable<T>
を直接扱うことができ、内部でGetEnumerator()
メソッドを呼び出して要素を順に取得します。
for
文はIEnumerable<T>
自体にはインデクサーがないため使えません。
IEnumerable<T>
をfor
文で扱いたい場合は、ToList()
やToArray()
で変換してからインデックスアクセスを行う必要があります。
以下はIEnumerable<int>
をforeach
文で処理する例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
IEnumerable<int> numbers = Enumerable.Range(1, 5); // 1から5までの数列
// foreach文でIEnumerableを処理
foreach (var num in numbers)
{
Console.WriteLine(num);
}
}
}
1
2
3
4
5
IEnumerable<T>
は遅延評価されることが多いため、foreach
文で順に処理するのが自然です。
for
文で使いたい場合は、ToList()
やToArray()
で一旦評価してから使うことを検討してください。
イテレーション対象 | for文の利用可否 | foreach文の利用可否 | 備考 |
---|---|---|---|
配列 | 〇 | 〇 | インデックス操作が可能 |
List<T> | 〇 | 〇 | インデックス操作が可能 |
Dictionary<TKey, TValue> | × | 〇 | インデックスなし、KeyValuePairで処理 |
IEnumerable<T>全般 | × | 〇 | インデックスなし、遅延評価に注意 |
このように、イテレーション対象によってfor
文とforeach
文の使い分けが変わります。
コレクションの特性を理解して適切なループ構文を選ぶことが大切です。
インデックス操作の要否
インデックス必須シナリオ
インデックスを使った操作が必要な場合は、for
文を使うのが適しています。
インデックスが必須となる代表的なシナリオをいくつか挙げてみます。
特定の位置の要素にアクセス・変更したい場合
配列やList<T>
の特定の位置にある要素を直接参照したり、値を変更したりする場合はインデックスが必要です。
foreach
文ではループ変数が読み取り専用のため、要素の直接書き換えはできません。
例えば、配列の偶数番目の要素だけを2倍にする処理はfor
文で書きます。
using System;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };
for (int i = 0; i < numbers.Length; i++)
{
if (i % 2 == 0) // 偶数インデックス
{
numbers[i] *= 2; // 値を2倍に変更
}
}
foreach (var num in numbers)
{
Console.WriteLine(num);
}
}
}
2
2
6
4
10
6
このように、インデックスを使うことで特定の位置の要素だけを選択的に操作できます。
ループの途中でインデックスを使って条件分岐や処理を変えたい場合
インデックスを使うことで、ループの進行状況に応じて処理を変えられます。
例えば、最初の数個だけ特別な処理をしたい場合や、最後の要素だけ別の処理をしたい場合などです。
using System;
class Program
{
static void Main()
{
string[] names = { "太郎", "花子", "次郎", "三郎" };
for (int i = 0; i < names.Length; i++)
{
if (i == 0)
{
Console.WriteLine($"最初の名前: {names[i]}");
}
else if (i == names.Length - 1)
{
Console.WriteLine($"最後の名前: {names[i]}");
}
else
{
Console.WriteLine($"中間の名前: {names[i]}");
}
}
}
}
最初の名前: 太郎
中間の名前: 花子
中間の名前: 次郎
最後の名前: 三郎
このように、インデックスを使うことでループの位置に応じた柔軟な処理が可能です。
ループの範囲を部分的に制御したい場合
配列やリストの一部だけを処理したい場合もインデックスが必要です。
for
文なら開始位置や終了位置を自由に設定できます。
using System;
class Program
{
static void Main()
{
int[] data = { 10, 20, 30, 40, 50, 60 };
// 2番目から4番目までの要素だけ処理
for (int i = 1; i <= 3; i++)
{
Console.WriteLine(data[i]);
}
}
}
20
30
40
foreach
文では全要素を順に処理するため、部分的な範囲指定はできません。
逆順で処理したい場合
逆順に要素を処理したいときもfor
文が適しています。
インデックスを使って最後の要素から最初の要素へループできます。
using System;
class Program
{
static void Main()
{
string[] items = { "A", "B", "C", "D" };
for (int i = items.Length - 1; i >= 0; i--)
{
Console.WriteLine(items[i]);
}
}
}
D
C
B
A
foreach
文は順方向のイテレーションのみサポートしているため、逆順処理には向きません。
インデックス不要シナリオ
インデックスを使わずに要素を順番に処理するだけで十分な場合は、foreach
文が適しています。
以下のようなシナリオが該当します。
コレクションの全要素を順に処理する場合
単純に全要素を順番に読み取り、何らかの処理を行うだけならforeach
文がシンプルでわかりやすいです。
using System;
class Program
{
static void Main()
{
string[] animals = { "犬", "猫", "鳥" };
foreach (var animal in animals)
{
Console.WriteLine(animal);
}
}
}
犬
猫
鳥
コードが短く、インデックスを気にしなくてよいので可読性が高まります。
要素の順序に沿って処理したい場合
foreach
文はコレクションの順序に従って要素を取り出すため、順序を保った処理が自然に書けます。
順序が重要な処理ではforeach
文が適しています。
コレクションの要素を変更しない場合
foreach
文のループ変数は読み取り専用なので、要素の値を変更しない処理に向いています。
例えば、要素の表示や集計などです。
コレクションの種類がIEnumerable<T>である場合
IEnumerable<T>
はインデックスを持たないため、for
文は使えません。
foreach
文でイテレーションするしかありません。
using System;
using System.Linq;
class Program
{
static void Main()
{
var query = Enumerable.Range(1, 3).Select(x => x * 10);
foreach (var value in query)
{
Console.WriteLine(value);
}
}
}
10
20
30
このように、foreach
文はインデックス不要で全要素を順に処理したい場合に最適です。
インデックス操作が必要かどうかで、for
文とforeach
文の使い分けが決まります。
インデックスを使って細かく制御したい場合はfor
文、単純に全要素を順に処理したい場合はforeach
文を選ぶとよいでしょう。
ループ中のコレクション変更
要素の追加・削除
ループ処理中にコレクションの要素を追加したり削除したりする場合、for
文とforeach
文で挙動が大きく異なります。
特にforeach
文ではコレクションの構造が変わると例外が発生しやすいため注意が必要です。
foreach文での追加・削除
foreach
文は内部的にコレクションのイテレーター(Enumerator)を使って要素を順に取得します。
このイテレーターはコレクションの状態が変わると整合性が崩れるため、ループ中に要素の追加や削除を行うとInvalidOperationException
が発生します。
以下はforeach
文でリストの要素を削除しようとして例外が発生する例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
try
{
foreach (var num in numbers)
{
if (num % 2 == 0)
{
numbers.Remove(num); // ループ中に削除
}
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine("例外発生: " + ex.Message);
}
}
}
例外発生: コレクションが変更されたため、列挙操作は無効になりました。
このように、foreach
文中でのコレクションの変更は基本的に禁止されています。
for文での追加・削除
for
文はインデックスを使ってループを制御しているため、ループ中に要素の追加や削除を行うことが可能です。
ただし、要素数やインデックスの変化に注意しないと、意図しない動作や例外が発生することがあります。
例えば、リストの偶数の要素を削除する場合は、インデックスを調整しながらループを回す必要があります。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
for (int i = 0; i < numbers.Count; i++)
{
if (numbers[i] % 2 == 0)
{
numbers.RemoveAt(i);
i--; // 削除で要素が詰まるためインデックスを戻す
}
}
foreach (var num in numbers)
{
Console.WriteLine(num);
}
}
}
1
3
5
このように、for
文ならループ中の追加・削除が可能ですが、インデックスの調整を忘れないようにしましょう。
要素値の変更
コレクションの要素自体の値を変更する場合は、for
文とforeach
文で挙動が異なります。
値型の要素の場合
foreach
文のループ変数は読み取り専用であり、値型の要素を直接変更することはできません。
ループ変数は要素のコピーを受け取るため、変更しても元のコレクションには反映されません。
以下はforeach
文で値型の要素を変更しようとしても反映されない例です。
using System;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3 };
foreach (var num in numbers)
{
// num = num * 2; // コンパイルエラー: 変更不可
}
// 変更したい場合はfor文を使う
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] *= 2;
}
foreach (var num in numbers)
{
Console.WriteLine(num);
}
}
}
2
4
6
foreach
文のループ変数は読み取り専用なので、値型の要素を変更したい場合はfor
文を使いましょう。
参照型の要素の場合
参照型の要素の場合は、foreach
文のループ変数は要素の参照を受け取るため、要素のプロパティやフィールドを変更することは可能です。
using System;
using System.Collections.Generic;
class Person
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
List<Person> people = new List<Person>
{
new Person { Name = "太郎" },
new Person { Name = "花子" }
};
foreach (var person in people)
{
person.Name += "さん"; // プロパティの変更は可能
}
foreach (var person in people)
{
Console.WriteLine(person.Name);
}
}
}
太郎さん
花子さん
このように、参照型の要素の内部状態はforeach
文でも変更できます。
例外が発生するケース
ループ中にコレクションを変更すると、特にforeach
文で例外が発生しやすいです。
代表的な例外とその原因をまとめます。
例外名 | 発生原因 | 対応策 |
---|---|---|
InvalidOperationException | foreach 中にコレクションの要素数や構造を変更 | for 文を使うか、コレクションのコピーを作成して処理 |
ArgumentOutOfRangeException | for 文でインデックスが範囲外になる | インデックスの調整やループ条件の見直し |
NullReferenceException | ループ中に要素がnullになりアクセスした場合 | nullチェックを行う |
特にforeach
文でのInvalidOperationException
は、コレクションの追加・削除が原因でよく発生します。
安全にループ中に要素を変更したい場合は、for
文を使うか、変更対象の要素を別リストにためてループ後にまとめて処理する方法が推奨されます。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> toRemove = new List<int>();
foreach (var num in numbers)
{
if (num % 2 == 0)
{
toRemove.Add(num); // 削除対象を別リストにためる
}
}
foreach (var num in toRemove)
{
numbers.Remove(num); // ループ外で削除
}
foreach (var num in numbers)
{
Console.WriteLine(num);
}
}
}
1
3
5
この方法なら例外を回避しつつ安全に要素の削除ができます。
ループ中のコレクション変更は、foreach
文では基本的に避け、for
文や別リストを使った方法で安全に行うことが重要です。
要素の値の変更は参照型か値型かで挙動が異なるため、適切な方法を選びましょう。
パフォーマンス比較の視点
ベンチマークの取り方
C#でfor
文とforeach
文のパフォーマンスを比較する際は、正確なベンチマークを行うことが重要です。
単純に処理時間を計測するだけでなく、JITコンパイルやGC(ガベージコレクション)の影響を考慮し、安定した結果を得るための工夫が必要です。
ベンチマークの基本手順
- ウォームアップ
最初の数回のループはJITコンパイルや初期化処理が含まれるため、計測から除外します。
ウォームアップを行い、実行環境を安定させます。
- 複数回の繰り返し実行
1回の計測だけでなく、複数回繰り返して平均値や中央値を取ることで、ばらつきを減らします。
- 高精度タイマーの使用
System.Diagnostics.Stopwatch
を使うと高精度な時間計測が可能です。
- GCの影響を抑える
ベンチマーク前にGC.Collect()
を呼び出してメモリを整理し、GC発生による計測誤差を減らします。
サンプルコード例
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
int[] data = new int[1000000];
for (int i = 0; i < data.Length; i++) data[i] = i;
// ウォームアップ
RunForLoop(data);
RunForeachLoop(data);
// GC整理
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var sw = new Stopwatch();
// for文計測
sw.Start();
RunForLoop(data);
sw.Stop();
Console.WriteLine($"for文: {sw.ElapsedMilliseconds} ms");
// GC整理
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// foreach文計測
sw.Restart();
RunForeachLoop(data);
sw.Stop();
Console.WriteLine($"foreach文: {sw.ElapsedMilliseconds} ms");
}
static void RunForLoop(int[] array)
{
long sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
}
static void RunForeachLoop(int[] array)
{
long sum = 0;
foreach (var item in array)
{
sum += item;
}
}
}
このように、計測前にウォームアップとGC整理を行い、Stopwatch
で時間を計測します。
配列での性能傾向
配列はメモリ上に連続して要素が格納されているため、インデックスアクセスが非常に高速です。
for
文はインデックスを直接使うため、配列の走査において最も効率的な方法の一つです。
foreach
文も配列に対しては内部的にインデックスアクセスを使うため、ほぼ同等のパフォーマンスを発揮します。
ただし、JITの最適化や環境によってはわずかにfor
文の方が高速になることがあります。
実際の傾向
- 大量の要素を処理する場合、
for
文がわずかに高速 - 小規模な配列では差がほとんど感じられない
foreach
文はコードがシンプルで可読性が高いでしょう
配列に対してはパフォーマンス差は微小ですが、最高速を求める場合はfor
文を選ぶことが多いです。
List<T>での性能傾向
List<T>
は内部的に配列を使っているため、インデックスアクセスは高速です。
for
文でインデックスを使うと、直接内部配列にアクセスするため効率的です。
foreach
文はList<T>
のGetEnumerator()
を使って要素を取得します。
List<T>
のイテレーターは構造体であり、ボクシングが発生しないためパフォーマンスは良好です。
ただし、for
文に比べるとわずかにオーバーヘッドがあります。
実際の傾向
for
文はインデックスアクセスで高速foreach
文はイテレーターを使うため若干のオーバーヘッドがあります- 大量データ処理では
for
文の方がわずかに速い傾向
// List<T>のfor文とforeach文の処理は配列と似た傾向を示します。
List<T>
ではパフォーマンスを重視するならfor
文が推奨されますが、可読性を優先するならforeach
文も十分実用的です。
Dictionary<TKey, TValue>での性能傾向
Dictionary<TKey, TValue>
はハッシュテーブルを内部構造として持ち、キーから値への高速な検索を実現しています。
イテレーションはキーと値のペアを列挙する形で行われます。
for
文はインデックスアクセスができないため、Dictionary
のイテレーションは基本的にforeach
文を使います。
イテレーションの特徴
foreach
文はDictionary
のGetEnumerator()
を使い、KeyValuePair<TKey, TValue>
を順に取得- 内部的にバケットを走査するため、配列やリストよりは若干遅い
- 順序は保証されないため、順序が重要な場合は別途ソートが必要でしょう
パフォーマンス傾向
Dictionary
のイテレーションはfor
文が使えないため、foreach
文一択- 大量の要素でも高速に列挙できるが、配列やリストよりは遅い
- キー検索は非常に高速だが、単純な全要素走査は配列やリストの方が速い
Dictionary
のイテレーションはforeach
文を使い、パフォーマンスは十分ですが、配列やリストと比べると若干劣ります。
用途に応じて使い分けましょう。
コードの可読性と保守性
シンプルさの評価
コードの可読性は、保守性やバグの発生を防ぐうえで非常に重要です。
for
文とforeach
文を比較すると、一般的にforeach
文の方がシンプルで読みやすいコードを書きやすい傾向があります。
foreach
文はコレクションの要素を順に取り出すことに特化しているため、インデックスの管理やループの境界条件を意識する必要がありません。
これにより、コードが短くなり、意図が明確に伝わりやすくなります。
例えば、配列の全要素を表示する場合、foreach
文は以下のように書けます。
string[] fruits = { "りんご", "みかん", "バナナ" };
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}
一方、for
文ではインデックスの初期化や条件式、インクリメントを明示的に書く必要があります。
for (int i = 0; i < fruits.Length; i++)
{
Console.WriteLine(fruits[i]);
}
このように、foreach
文はループの構造がシンプルで、コードの意図が直感的に理解しやすいです。
ただし、for
文はインデックスを使った細かい制御が必要な場合に適しており、その場合は多少複雑になってもfor
文を使う方が適切です。
バグ発生リスク
for
文はインデックスの管理が必要なため、境界条件のミスやインデックスの誤操作によるバグが発生しやすいです。
例えば、ループの終了条件を間違えて配列の範囲外にアクセスしてしまうと、IndexOutOfRangeException
が発生します。
int[] numbers = { 1, 2, 3 };
// バグ例: i <= numbers.Length は範囲外アクセスになる
for (int i = 0; i <= numbers.Length; i++)
{
Console.WriteLine(numbers[i]); // 例外発生
}
一方、foreach
文はインデックスを使わないため、こうした範囲外アクセスのリスクがありません。
コレクションの全要素を安全に処理できるため、バグの発生を抑えやすいです。
ただし、foreach
文でもループ中にコレクションを変更するとInvalidOperationException
が発生するため、コレクションの変更には注意が必要です。
チーム開発での推奨方針
チーム開発においては、コードの可読性と保守性を重視し、統一したコーディングスタイルを採用することが重要です。
- 基本は
foreach
文を使う
全要素を順に処理する場合はforeach
文を使い、シンプルで読みやすいコードを書くことを推奨します。
これにより、メンバー間での理解がスムーズになります。
- インデックス操作が必要な場合は
for
文を使う
特定の要素だけを処理したり、逆順で処理したりする場合はfor
文を使います。
ただし、インデックスの管理ミスを防ぐために、コードレビューで注意深くチェックすることが大切です。
- コレクションの変更はループ外で行う
ループ中のコレクション変更はバグの温床になるため、可能な限りループ外で処理するか、for
文で慎重に扱うことをルール化するとよいでしょう。
- コードレビューとペアプログラミングの活用
ループ処理はバグが入り込みやすい箇所なので、コードレビューやペアプログラミングで相互チェックを行い、品質を保つことが推奨されます。
このように、チーム開発ではforeach
文を基本としつつ、必要に応じてfor
文を使い分ける方針が、可読性と保守性の両立に役立ちます。
逆順・スキップ・早期終了のテクニック
逆順走査
配列やリストなどのコレクションを逆順に走査したい場合、for
文が最も適しています。
for
文ではループ変数の初期値をコレクションの最後のインデックスに設定し、条件式で0以上を指定、ループごとにデクリメントi--
することで逆順に処理できます。
using System;
class Program
{
static void Main()
{
string[] items = { "A", "B", "C", "D" };
Console.WriteLine("逆順走査(for文):");
for (int i = items.Length - 1; i >= 0; i--)
{
Console.WriteLine($"インデックス {i}: {items[i]}");
}
}
}
逆順走査(for文):
インデックス 3: D
インデックス 2: C
インデックス 1: B
インデックス 0: A
一方、foreach
文はコレクションの順方向のイテレーションに特化しているため、逆順走査には向いていません。
逆順で処理したい場合は、for
文を使うか、Enumerable.Reverse()
メソッドを使って逆順の列挙子を取得してからforeach
文で処理する方法があります。
using System;
using System.Linq;
class Program
{
static void Main()
{
string[] items = { "A", "B", "C", "D" };
Console.WriteLine("逆順走査(Enumerable.Reverse + foreach):");
foreach (var item in items.Reverse())
{
Console.WriteLine(item);
}
}
}
逆順走査(Enumerable.Reverse + foreach):
D
C
B
A
ただし、Enumerable.Reverse()
は内部で新しい列挙子を生成するため、パフォーマンス面ではfor
文の逆順走査の方が効率的です。
条件付きスキップ
ループ中に特定の条件を満たす要素をスキップして処理したい場合、continue
文を使います。
continue
は現在のループの残りの処理をスキップし、次のループの繰り返しに移ります。
以下は、偶数の要素をスキップして奇数だけを表示する例です。
using System;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine("奇数のみ表示(continue使用):");
foreach (var num in numbers)
{
if (num % 2 == 0)
{
continue; // 偶数はスキップ
}
Console.WriteLine(num);
}
}
}
奇数のみ表示(continue使用):
1
3
5
for
文でも同様にcontinue
を使って条件付きスキップが可能です。
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] % 2 == 0)
{
continue; // 偶数はスキップ
}
Console.WriteLine(numbers[i]);
}
このように、continue
はループの中で特定の条件を満たす場合に処理を飛ばしたいときに便利です。
breakとcontinueの挙動差
break
とcontinue
はどちらもループ制御に使われますが、挙動は大きく異なります。
キーワード | 挙動の説明 |
---|---|
break | ループを即座に終了し、ループの外の処理に移る |
continue | 現在のループの残りの処理をスキップし、次の繰り返しに移る |
breakの例
以下は、配列の中で最初に3の倍数が見つかったらループを終了する例です。
using System;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 4, 6, 9, 12 };
Console.WriteLine("最初の3の倍数を見つけたら終了(break使用):");
foreach (var num in numbers)
{
if (num % 3 == 0)
{
Console.WriteLine($"3の倍数発見: {num}");
break; // ループ終了
}
Console.WriteLine(num);
}
}
}
最初の3の倍数を見つけたら終了(break使用):
1
2
4
3の倍数発見: 6
continueの例
先述の条件付きスキップの例のように、continue
は条件を満たす場合に処理をスキップして次のループに進みます。
使い分けのポイント
break
はループを途中で完全に終了したいときに使います。例えば、目的の要素を見つけたらそれ以上処理する必要がない場合などですcontinue
は特定の条件のときだけ処理をスキップし、ループは継続したい場合に使います
この2つを適切に使い分けることで、ループ処理の効率化やコードの明確化が図れます。
カスタムイテレーターとの連携
IEnumerableとIEnumeratorの役割
C#のコレクションをforeach
文で繰り返し処理できるのは、IEnumerable
とIEnumerator
というインターフェースの仕組みがあるからです。
これらはイテレーション(列挙)を実現するための基本的な役割を担っています。
- IEnumerable
IEnumerable
は「列挙可能なオブジェクト」を表すインターフェースで、GetEnumerator()
メソッドを持ちます。
このメソッドはIEnumerator
を返し、コレクションの要素を順に取得するための列挙子を提供します。
ジェネリック版のIEnumerable<T>
もあり、こちらは型安全に要素を列挙できます。
- IEnumerator
IEnumerator
は実際にコレクションの要素を順に取得するためのインターフェースです。
主に以下のメンバーを持ちます。
Current
プロパティ:現在の要素を取得しますMoveNext()
メソッド:次の要素に進み、要素があればtrue
を返しますReset()
メソッド:列挙子を初期位置に戻します(ほとんど使われません)
foreach
文は内部的にIEnumerable
のGetEnumerator()
を呼び出し、返されたIEnumerator
を使ってMoveNext()
とCurrent
を繰り返し呼び出すことで要素を順に処理しています。
yield returnを使った自作イテレーター
C#ではyield return
を使うことで、簡単にカスタムイテレーターを作成できます。
yield return
はメソッドの中で要素を1つずつ返し、呼び出し元に遅延的に列挙可能なシーケンスを提供します。
以下はyield return
を使った自作イテレーターの例です。
1から指定した数までの整数を順に返すメソッドです。
using System;
using System.Collections.Generic;
class Program
{
static IEnumerable<int> GetNumbers(int max)
{
for (int i = 1; i <= max; i++)
{
yield return i; // 1つずつ返す
}
}
static void Main()
{
foreach (var num in GetNumbers(5))
{
Console.WriteLine(num);
}
}
}
1
2
3
4
5
この例では、GetNumbers
メソッドがIEnumerable<int>
を返し、yield return
で1つずつ値を返しています。
呼び出し側のforeach
文はこのシーケンスを順に処理します。
yield return
を使うと、複雑な状態管理やIEnumerator
の実装を自分で書かなくても、簡潔にイテレーターを作成できるのが大きなメリットです。
foreachによるGetEnumerator呼び出し
foreach
文はコンパイラによって以下のような処理に変換されます。
- ループ対象のオブジェクトの
GetEnumerator()
メソッドを呼び出し、IEnumerator
(またはIEnumerator<T>
)を取得します。 MoveNext()
を呼び出して次の要素があるか確認します。- 要素があれば
Current
プロパティで現在の要素を取得し、ループ本体の処理を実行します。 - 2~3を繰り返します。
- 列挙子が
IDisposable
を実装していれば、ループ終了時にDispose()
を呼び出します。
以下はforeach
文の展開例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
int[] numbers = { 10, 20, 30 };
// foreach文
foreach (var num in numbers)
{
Console.WriteLine(num);
}
// 上記は以下のように展開される
/*
var enumerator = numbers.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var num = (int)enumerator.Current;
Console.WriteLine(num);
}
}
finally
{
(enumerator as IDisposable)?.Dispose();
}
*/
}
}
このように、foreach
文はGetEnumerator()
を呼び出して列挙子を取得し、MoveNext()
とCurrent
を使って要素を順に処理しています。
これにより、IEnumerable
を実装していればどんなコレクションでもforeach
文で簡単にループ処理が可能になります。
カスタムイテレーターを作る際は、yield return
を活用して簡潔に実装し、foreach
文で自然に使えるようにするのがC#の標準的な方法です。
IEnumerable
とIEnumerator
の仕組みを理解すると、コレクションの拡張や独自の列挙処理を柔軟に設計できます。
非同期・並列処理とループ
async/awaitとforeach
C#のasync
/await
を使った非同期処理とforeach
文を組み合わせる際には、いくつか注意点があります。
特に、非同期メソッドをループ内で順番に実行する場合と並列に実行する場合で書き方が異なります。
順次非同期処理(awaitをループ内で使う)
非同期メソッドをforeach
の中でawait
すると、各要素の処理が順番に完了するまで次のループに進みません。
つまり、逐次的に非同期処理が実行されます。
using System;
using System.Threading.Tasks;
class Program
{
static async Task ProcessItemAsync(int item)
{
await Task.Delay(500); // 擬似的な非同期処理
Console.WriteLine($"処理完了: {item}");
}
static async Task Main()
{
int[] items = { 1, 2, 3 };
foreach (var item in items)
{
await ProcessItemAsync(item); // 順次処理
}
}
}
処理完了: 1
処理完了: 2
処理完了: 3
この場合、1つ目の処理が終わってから2つ目、3つ目と順に処理されるため、全体の処理時間は各処理時間の合計になります。
並列非同期処理(Taskをまとめて実行)
複数の非同期処理を並列に実行したい場合は、foreach
でTask
を作成し、それらをTask.WhenAll
でまとめて待機します。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task ProcessItemAsync(int item)
{
await Task.Delay(500);
Console.WriteLine($"処理完了: {item}");
}
static async Task Main()
{
int[] items = { 1, 2, 3 };
var tasks = new List<Task>();
foreach (var item in items)
{
tasks.Add(ProcessItemAsync(item)); // タスクを作成して追加
}
await Task.WhenAll(tasks); // すべてのタスクの完了を待つ
}
}
処理完了: 3
処理完了: 2
処理完了: 1
この方法だと、全ての処理がほぼ同時に開始され、全体の処理時間は最も遅い処理時間に近くなります。
Parallel.Forの特徴
Parallel.For
は.NETの並列処理ライブラリであるSystem.Threading.Tasks
名前空間に含まれ、複数のスレッドを使ってループ処理を並列化します。
CPUバウンドな処理の高速化に適しています。
基本的な使い方
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] data = new int[10];
Parallel.For(0, data.Length, i =>
{
data[i] = i * i;
Console.WriteLine($"Index {i}: {data[i]} (Thread {Task.CurrentId})");
});
}
}
Index 3: 9 (Thread 7)
Index 6: 36 (Thread 5)
Index 4: 16 (Thread 6)
Index 9: 81 (Thread 10)
Index 2: 4 (Thread 4)
Index 5: 25 (Thread 8)
Index 1: 1 (Thread 1)
Index 7: 49 (Thread 3)
Index 8: 64 (Thread 2)
Index 0: 0 (Thread 9)
Parallel.For
はループの各イテレーションを複数のスレッドに分散して実行するため、CPUコアを有効活用できます。
特徴と注意点
- スレッドプールを利用し、スレッドの生成コストを抑えつつ並列処理を実現
- ループの順序は保証されないため、順序依存の処理には向かない
- スレッドセーフな処理が必要。共有リソースへのアクセスは排他制御が必要でしょう
- I/Oバウンド処理には不向き。CPUバウンド処理に適しています
PLINQを用いたデータ並列
PLINQ(Parallel LINQ)はLINQクエリを並列化して高速化するための機能です。
AsParallel()
メソッドを使うことで、簡単にデータ並列処理が可能になります。
基本例
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = Enumerable.Range(1, 10).ToArray();
var squares = numbers.AsParallel()
.Select(x => x * x)
.ToArray();
foreach (var sq in squares)
{
Console.WriteLine(sq);
}
}
}
1
4
9
16
25
36
49
64
81
100
AsParallel()
を呼び出すだけで、LINQのSelect
やWhere
などの演算が複数スレッドで並列実行されます。
特徴と注意点
- 簡単に並列化できるため、既存のLINQクエリを高速化しやすい
- 順序保証はデフォルトで無効。順序を保ちたい場合は
AsOrdered()
を使います - 副作用のある処理は避けるべき。並列実行で予期せぬ動作になる可能性があります
- I/Oバウンド処理よりCPUバウンド処理に適している
非同期処理や並列処理をループで扱う際は、処理の性質(CPUバウンドかI/Oバウンドか)、順序の必要性、スレッドセーフ性などを考慮して適切な方法を選ぶことが重要です。
null許容参照型対応
foreachのnullチェックパターン
C# 8.0以降で導入されたnull許容参照型(Nullable Reference Types)を使うと、参照型変数がnullになる可能性をコンパイル時に検出しやすくなります。
foreach
文でコレクションの要素を扱う際も、null許容型に対応した安全なコードを書くことが重要です。
foreach
文でコレクションの要素がnullの可能性がある場合、ループ内でnullチェックを行うのが基本的なパターンです。
例えば、文字列の配列でnullが含まれている可能性がある場合は以下のように書きます。
using System;
class Program
{
static void Main()
{
string?[] names = { "太郎", null, "花子" };
foreach (var name in names)
{
if (name is null)
{
Console.WriteLine("名前がnullです");
continue; // nullの場合はスキップ
}
Console.WriteLine($"名前: {name}");
}
}
}
名前: 太郎
名前がnullです
名前: 花子
このように、foreach
のループ変数がnull許容型の場合は、if (name is null)
やif (name == null)
で明示的にnullチェックを行い、nullの要素を安全に処理またはスキップします。
また、null
を許容しないコレクションであっても、外部からの入力やAPIの仕様によってnullが混入する可能性がある場合は、念のためnullチェックを入れることが推奨されます。
forを用いる安全策
for
文を使う場合も、null許容参照型に対応した安全なコードを書くことが大切です。
for
文ではインデックスを使って要素にアクセスするため、コレクション自体や要素がnullでないかを事前にチェックすることがポイントです。
以下は、for
文でnull許容型の配列を安全に処理する例です。
using System;
class Program
{
static void Main()
{
string?[] names = { "太郎", null, "花子" };
if (names is null)
{
Console.WriteLine("配列自体がnullです");
return;
}
for (int i = 0; i < names.Length; i++)
{
var name = names[i];
if (name is null)
{
Console.WriteLine($"インデックス {i} の名前がnullです");
continue; // nullの場合はスキップ
}
Console.WriteLine($"インデックス {i} の名前: {name}");
}
}
}
インデックス 0 の名前: 太郎
インデックス 1 の名前がnullです
インデックス 2 の名前: 花子
この例では、
- 配列自体がnullでないかを最初にチェックしています
- 各要素がnullかどうかをループ内で判定し、nullの場合はスキップしています
for
文はインデックスを使うため、nullチェックの際にどの位置の要素がnullかを特定しやすいメリットがあります。
null許容参照型対応では、foreach
でもfor
でも要素のnullチェックを怠らず、nullが混入しても安全に処理できるコードを書くことが重要です。
特に外部からのデータを扱う場合は、nullチェックを徹底してバグや例外の発生を防ぎましょう。
選択基準のチェックリスト
目的別の推奨選択
for
文とforeach
文の使い分けは、処理の目的や状況によって最適な選択が変わります。
以下のポイントを参考に、目的別にどちらを使うべきかを判断してください。
目的・状況 | 推奨ループ構文 | 理由・ポイント |
---|---|---|
コレクションの全要素を順に処理したい | foreach | インデックス管理不要でシンプル、可読性が高い |
要素のインデックスが必要 | for | インデックスを使ったアクセスや条件分岐が可能 |
逆順に処理したい | for | foreach は順方向のみ、逆順はfor で制御可能 |
ループ中に要素の追加・削除を行う | for | foreach はコレクション変更で例外が発生しやすい |
参照型要素のプロパティを変更したい | foreach | 参照型の内部状態はforeach でも変更可能 |
値型要素の値を変更したい | for | foreach のループ変数は読み取り専用で変更不可 |
コレクションがIEnumerable<T> のみ | foreach | for はインデックス不要なため使えない |
パフォーマンスを最大限重視 | for | インデックスアクセスでわずかに高速な場合が多い |
コードの可読性・保守性を重視 | foreach | シンプルでバグが入りにくい |
このように、処理の内容やコレクションの種類、パフォーマンス要件に応じて使い分けることが重要です。
無理にfor
文を使うよりも、シンプルで安全なforeach
文を基本にし、必要に応じてfor
文を選択するとよいでしょう。
コードレビュー観点
コードレビューの際にfor
文とforeach
文の使い分けをチェックするポイントをまとめます。
適切なループ構文を選んでいるか、バグやパフォーマンス問題のリスクがないかを確認しましょう。
- インデックスが必要かどうかの判断
インデックスを使っていないのにfor
文を使っていないか。
不要なインデックス管理は可読性を下げるため、foreach
に置き換えられるか検討します。
- ループ中のコレクション変更の有無
foreach
文内でコレクションの追加・削除が行われていないか。
もしあれば例外の原因になるため、for
文や別の方法に修正を促します。
- 値型要素の変更処理
値型の要素をforeach
内で変更しようとしていないか。
変更が必要ならfor
文に切り替えるべき。
- 逆順処理の適切な実装
逆順処理が必要な場合にforeach
を使っていないか。
for
文やEnumerable.Reverse()
の利用を提案します。
- パフォーマンス要件の確認
大量データ処理でパフォーマンスが重要な場合、for
文の方が適していることを伝えます。
- null許容参照型対応の有無
ループ内でnullチェックが適切に行われているか。
特にforeach
のループ変数がnull許容型の場合は必須。
- コードの可読性と一貫性
チームのコーディング規約に沿ったループ構文が使われているか。
統一感があるかを確認します。
これらの観点を踏まえ、レビュー時には単に動作するかだけでなく、将来的な保守性やバグ防止の観点からもループ構文の選択を評価しましょう。
適切な使い分けができているコードは、チーム全体の品質向上につながります。
まとめ
この記事では、C#のfor
文とforeach
文の基本構文から、用途別の使い分けポイント、パフォーマンスや可読性、ループ中のコレクション変更時の注意点まで幅広く解説しました。
配列やリスト、辞書などのコレクション特性に応じて適切なループを選び、インデックス操作の必要性や非同期・並列処理との連携も理解できます。
安全で効率的なコードを書くために、目的に合ったループ構文を選択し、チーム開発でのベストプラクティスを意識することが重要です。