C#を用いたシステム開発において、繰り返し処理(ループ処理)はプログラムの論理構造を支える最も重要な要素の一つです。
同じ処理を何度も記述する手間を省くだけでなく、配列やリストといったデータ集合を効率的に操作するために欠かせません。
C#には for、foreach、while、do-while という4つの主要なループ構文が用意されており、それぞれに得意な場面と不得意な場面が存在します。
本記事では、プロの視点から各構文の詳細な使い方、パフォーマンスの違い、そして実務で役立つ状況別の使い分けガイドを徹底的に解説します。
C#における繰り返し処理の基本概念
C#で繰り返し処理を記述する際、私たちが意識すべきは「何回繰り返すか(または何に対して繰り返すか)」と「どのような条件で終了するか」の2点です。
例えば、数値が100になるまでカウントアップしたい場合や、データベースから取得したユーザー一覧の全員にメールを送信したい場合など、目的によって最適な構文は異なります。
不適切な構文を選択すると、コードの可読性が低下するだけでなく、無限ループによるプログラムのハングアップや、メモリ効率の悪化を招くリスクがあります。
最新のC#(.NET 8/9以降)では、従来のループに加えて Span<T> を活用した高速な反復処理や、LINQを用いた宣言的な記述も一般的になっています。
しかし、それらの基盤となるのはこれから解説する基本のループ構文です。
まずはそれぞれの特徴を深く理解していきましょう。
for文:回数指定とインデックス操作の基本
for 文は、繰り返す回数が明確に決まっている場合や、配列のインデックス(添え字)を直接操作したい場合に最も適した構文です。
for文の基本構文
for 文は、「初期化式」「条件式」「変化式」の3つの要素で構成されます。
using System;
public class ForExample
{
public static void Main()
{
// 0から4まで、合計5回繰り返す
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"現在のカウント: {i}");
}
}
}
現在のカウント: 0
現在のカウント: 1
現在のカウント: 2
現在のカウント: 3
現在のカウント: 4
インデックスを利用した配列操作
for 文の強みは、ループ変数 i を使ってコレクションの特定の要素にアクセスしたり、複数の要素を同時に参照したりできる点にあります。
using System;
public class ArrayForExample
{
public static void Main()
{
int[] numbers = { 10, 20, 30, 40, 50 };
// インデックスを使って逆順に表示する
for (int i = numbers.Length - 1; i >= 0; i--)
{
Console.WriteLine($"インデックス {i} の値: {numbers[i]}");
}
}
}
インデックス 4 の値: 50
インデックス 3 の値: 40
インデックス 2 の値: 30
インデックス 1 の値: 20
インデックス 0 の値: 10
このように、i-- を使った逆順ループや、i += 2 による1つ飛ばしの処理などは for 文ならではの柔軟な操作です。
foreach文:コレクション反復のデファクトスタンダード
foreach 文は、配列や List<T> などのコレクション内のすべての要素に対して処理を行う場合に最適です。
C#において最も頻繁に使用されるループ構文と言えます。
foreach文のメリットと書き方
foreach の最大の特徴は、「インデックス管理を隠蔽してくれる」ことです。
これにより、境界外アクセス(IndexOutOfRangeException)のリスクがなくなり、コードが非常にシンプルになります。
using System;
using System.Collections.Generic;
public class ForeachExample
{
public static void Main()
{
var fruits = new List<string> { "Apple", "Banana", "Cherry" };
// コレクション内の各要素を自動的に取り出す
foreach (var fruit in fruits)
{
Console.WriteLine($"フルーツ名: {fruit}");
}
}
}
フルーツ名: Apple
フルーツ名: Banana
フルーツ名: Cherry
foreach文の注意点:要素の変更禁止
foreach ループの中で、反復中のコレクション自体を書き換える(要素の追加や削除を行う)ことはできません。
これを実行しようとすると、実行時に InvalidOperationException がスローされます。
また、反復変数(上記の例では fruit)は読み取り専用となります。
要素の中身を直接書き換えたい場合は、前述の for 文を使用するか、参照型であればそのプロパティを操作する必要があります。
内部構造:IEnumerable と IEnumerator
foreach は、内部的に IEnumerable インターフェースを利用しています。
コンパイラは foreach を GetEnumerator() メソッドの呼び出しと while ループ、そして IDisposable の処理(try-finallyブロック)に展開します。
このため、自作のクラスでも IEnumerable を実装していれば foreach で回すことが可能です。
while文とdo-while文:条件に応じた柔軟な反復
while 系は、「回数は決まっていないが、特定の条件が満たされている間はずっと続けたい」という状況で力を発揮します。
while文:前判定ループ
while 文は、ループの開始前に条件式を評価します。
条件が最初から false であれば、中身は一度も実行されません。
using System;
public class WhileExample
{
public static void Main()
{
int energy = 3;
// エネルギーがある間は行動を繰り返す
while (energy > 0)
{
Console.WriteLine($"行動中... 残りエネルギー: {energy}");
energy--; // 条件を変化させる
}
Console.WriteLine("動けなくなりました。");
}
}
行動中... 残りエネルギー: 3
行動中... 残りエネルギー: 2
行動中... 残りエネルギー: 1
動けなくなりました。
do-while文:後判定ループ
do-while 文は、処理を実行した後に条件を評価します。
そのため、最低でも必ず1回は処理が実行されることが保証されます。
ユーザー入力を受け取ってバリデーションを行う場合などに便利です。
using System;
public class DoWhileExample
{
public static void Main()
{
string input;
do
{
Console.Write("終了するには 'exit' と入力してください: ");
input = Console.ReadLine();
} while (input != "exit");
Console.WriteLine("プログラムを終了します。");
}
}
ループ制御の応用:break、continue、return
ループの途中で処理を中断したり、特定の回だけスキップしたりするための制御構文を理解することで、より高度なロジックを組むことができます。
break:ループの強制終了
break を使用すると、現在のループを即座に脱出します。
検索処理などで目的のデータが見つかった瞬間にループを止めたい場合に有効です。
continue:現在の反復をスキップ
continue は、それ以降の処理を飛ばして、次の回のループ判定へ移動します。
「特定の条件に合致するデータだけ処理を除外したい」というケースで使用します。
using System;
public class ControlExample
{
public static void Main()
{
for (int i = 1; i <= 5; i++)
{
if (i == 3)
{
Console.WriteLine("3をスキップします");
continue; // 次の i=4 へ
}
if (i == 5)
{
Console.WriteLine("5に到達したので終了します");
break; // ループ終了
}
Console.WriteLine($"数値: {i}");
}
}
}
数値: 1
数値: 2
3をスキップします
数値: 4
5に到達したので終了します
状況別:どのループを使うべきか?(比較表)
各構文の特性を整理し、使い分けの判断基準をまとめました。
| 構文 | 推奨されるケース | メリット | デメリット・注意点 |
|---|---|---|---|
| for | 回数指定、配列の添え字操作、逆順処理 | 柔軟性が高く、インデックスを直接扱える | コードが冗長になりやすく、境界ミスが起きやすい |
| foreach | リストや配列の全要素参照、読み取り主体 | 安全で可読性が極めて高い | 要素の削除・追加ができず、インデックスが直接取れない |
| while | 条件ベースのループ(通信待ち、状態変化待ち) | 終了条件が動的な場合に最適 | 無限ループの危険性が最も高い |
| do-while | 最低1回は実行が必要な処理(ユーザー入力など) | 初回の実行を保証できる | while 同様、終了条件の管理に注意が必要 |
迷った時の判断フロー
- コレクション(List, Array等)の全要素を処理したいか?
コレクションの全要素を順に処理する場合は、要素単位で反復するforeach(または各言語の対応構文)を選択してください。
例:
foreach (item in collection)。- インデックス番号が必要、または「n回おき」「逆順」などの特殊な回し方が必要か?
インデックスや増分・減分など細かい制御が必要な場合は、明示的なカウンタを使えるforを選びます。
例:
for (i = 0; i < n; i++)(逆順やステップ指定も可能)。- 回数ではなく、特定の条件(フラグや状態)が変わるまで続けたいか?
継続条件に基づいて繰り返す場合は、判定位置に応じて選択します。
事前に条件を判定するならwhile(例:
while (condition))、一度は必ず実行してから判定するならdo-while(例:do { ... } while (condition);)。
実務で役立つ高度な繰り返し処理
現代のC#開発では、基本的なループに加えて知っておくべき手法がいくつかあります。
1. Span<T> による高速反復
パフォーマンスが要求される場面(画像処理や大量のデータ解析など)では、Span<T> を使用します。
Span<T> に対する foreach は、コンパイラによって最適化され、通常の配列アクセスよりも高速になる場合があります。
2. Parallel.ForEach による並列処理
マルチコアCPUを最大限に活用したい場合、System.Threading.Tasks.Parallel.ForEach を検討します。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class ParallelExample
{
public static void Main()
{
var data = new List<int> { 1, 2, 3, 4, 5 };
// 各要素の処理を並列(マルチスレッド)で実行
Parallel.ForEach(data, item =>
{
Console.WriteLine($"アイテム {item} を処理中 (スレッドID: {Environment.CurrentManagedThreadId})");
});
}
}
※実行結果の順序は実行のたびに変わります。
3. LINQ (Enumerable.Repeat/Range)
特定の回数分だけ何かのリストを作りたい場合などは、ループを書かずに LINQ を使うのが現代的な書き方です。
// 1から10までの数値を生成して処理
foreach (var n in Enumerable.Range(1, 10))
{
// ...
}
アンチパターンと注意点
無限ループの回避
特に while 文を使用する際、「終了条件がいつまでも満たされない」状態が発生しないよう注意してください。
ループ内で条件に関わる変数が正しく更新されているか(例:i++ を忘れていないか)を必ず確認しましょう。
ネスト(多重ループ)の深さ
ループの中にループを入れる「二重ループ」などは便利ですが、データの数(N)が増えると計算量が指数関数的に増大します(O(N^2))。
1万件のデータに対して二重ループを回すと1億回の計算になり、パフォーマンス低下の主因となります。
このような場合は、Dictionary<K, V> を使ったハッシュマップ検索への置き換えなどを検討すべきです。
まとめ
C#の繰り返し処理は、用途に応じて使い分けることでその真価を発揮します。
- foreach は「安全性と可読性」を優先する際の第一選択肢。
- for は「柔軟なインデックス操作」が必要な場合に使用。
- while / do-while は「回数ではなく条件」で制御する場合に活用。
これらを適切に選択できるようになれば、バグが少なくメンテナンス性の高いコードが書けるようになります。
まずは基本となる foreach と for を完璧に使いこなせるようになり、必要に応じて while や並列処理などの応用テクニックを取り入れていきましょう。
