繰り返し文

【C#】foreachで配列を安全かつ効率的にループする基本と活用テクニック

foreachは配列の要素を先頭から順に読み取る最も手軽な方法です。

インデックスを意識せず要素にアクセスでき、境界チェックも不要なのでバグを減らせます。

ループ中に要素追加や削除はできないため構造を変えたい場合はforが向きます。

読み取り専用ループと捉えると扱いやすいです。

目次から探す
  1. foreach文とは
  2. 基本構文
  3. 型安全性
  4. スコープと読み取り専用性
  5. パフォーマンスの視点
  6. 多次元配列の処理
  7. 制御フロー
  8. 例外とエラーハンドリング
  9. null 許容参照型(NRT)との併用
  10. 拡張機能との連携
  11. async/await と非同期列挙
  12. カスタム列挙子
  13. ref foreach(C# 7.3以降)
  14. 破棄可能型配列の列挙
  15. ベストプラクティス集
  16. 参考コード断片
  17. まとめ

foreach文とは

C#におけるforeach文は、配列やリストなどのコレクションの要素を順番に処理するための構文です。

for文と異なり、インデックスを明示的に管理する必要がなく、コードがシンプルで読みやすくなるのが特徴です。

ここでは、foreach文の基本的な役割と、特に配列に対して使う際のメリットについて解説します。

foreachループの役割

foreach文は、コレクションの各要素を一つずつ取り出して処理を行うためのループ構文です。

例えば、配列やリスト、セット、辞書など、IEnumerableインターフェースを実装しているオブジェクトに対して使えます。

foreach文の主な役割は以下の通りです。

  • 要素の順次アクセス

コレクションの最初の要素から最後の要素まで順番にアクセスし、処理を行います。

インデックスを意識せずに済むため、コードが簡潔になります。

  • 安全な反復処理

ループ変数は読み取り専用であるため、誤ってループ中に要素を変更してしまうリスクが減ります。

また、コレクションの構造を壊すことなく反復処理が可能です。

  • 可読性の向上

インデックス管理や境界チェックのコードが不要になるため、処理内容に集中でき、コードの可読性が高まります。

以下は、整数配列の要素をforeachで順に表示する例です。

int[] numbers = { 10, 20, 30, 40, 50 };
foreach (int number in numbers)
{
    Console.WriteLine(number);
}

このコードは、配列numbersの各要素を順にnumberに代入し、コンソールに出力しています。

for文で書く場合に比べて、インデックスの初期化や条件式、増分処理を書く必要がなく、シンプルです。

配列に特化したメリット

foreach文は配列に対して特に効果的に使えます。

配列は固定長で連続したメモリ領域に要素が格納されているため、foreach文は内部的に高速に要素を走査できます。

ここでは配列に特化したforeachのメリットを紹介します。

インデックス管理不要でミスを減らせる

配列の要素をfor文で処理する場合、インデックスの初期値や終了条件を間違えると、範囲外アクセスや無限ループの原因になります。

foreach文はインデックスを意識しないため、こうしたミスを防げます。

読み取り専用のループ変数で安全性が高い

foreachのループ変数は読み取り専用で、配列の要素を直接書き換えることはできません。

これにより、ループ中に誤って配列の内容を変更してしまうリスクが減ります。

配列の内容を変更したい場合は、for文を使うか、別途処理を行う必要があります。

JITコンパイラによる最適化が期待できる

C#のJITコンパイラは、配列に対するforeach文を効率的に最適化します。

配列の要素は連続したメモリに格納されているため、foreach文は高速に走査でき、パフォーマンス面でも優れています。

コードの可読性と保守性が向上する

配列の要素を処理するコードが簡潔になるため、他の開発者がコードを理解しやすくなります。

特に大規模なプロジェクトやチーム開発では、可読性の高いコードは保守性の向上に直結します。

多次元配列やジャグ配列にも対応可能

foreach文は多次元配列やジャグ配列(配列の配列)にも使えます。

ネストしたforeach文を使うことで、複雑な配列構造も簡単に走査できます。

以上のように、foreach文は配列の要素を安全かつ効率的に処理するための強力なツールです。

基本構文

シンプルな整数配列

整数型の配列に対してforeach文を使う基本的な例を示します。

配列numbersの各要素を順に取り出し、コンソールに表示します。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        foreach (int number in numbers)
        {
            Console.WriteLine(number); // 配列の要素を1つずつ表示
        }
    }
}
1
2
3
4
5

このコードでは、foreachのループ変数numberが配列numbersの各要素を順に受け取ります。

for文のようにインデックスを使わずに済むため、コードがシンプルで読みやすくなっています。

文字列配列

文字列型の配列でも同様にforeach文を使えます。

以下は、果物の名前を格納した文字列配列fruitsの各要素を表示する例です。

using System;
class Program
{
    static void Main()
    {
        string[] fruits = { "りんご", "みかん", "ぶどう" };
        foreach (string fruit in fruits)
        {
            Console.WriteLine(fruit); // 各果物の名前を表示
        }
    }
}
りんご
みかん
ぶどう

文字列配列の場合も、foreach文の使い方は整数配列と変わりません。

ループ変数fruitが配列の各要素を順に受け取り、処理を行います。

var の活用

foreach文のループ変数の型は明示的に指定することが多いですが、varキーワードを使うことで型推論を利用できます。

特に配列の型が長い場合や、コードを簡潔にしたい場合に便利です。

using System;
class Program
{
    static void Main()
    {
        var numbers = new int[] { 10, 20, 30 };
        foreach (var number in numbers)
        {
            Console.WriteLine(number); // varで型推論されたnumberを表示
        }
    }
}
10
20
30

この例では、numbers配列の型はint[]ですが、varを使うことで明示的にintと書かずに済みます。

ループ変数numbervarで宣言しているため、コンパイラが自動的に型を推論します。

varを使うことでコードがすっきりし、特に複雑な型のコレクションを扱う際に可読性が向上します。

ただし、型が不明瞭になる場合は明示的に型を指定したほうが理解しやすいこともあります。

型安全性

暗黙的型変換と明示的型宣言

foreach文でループ変数の型を指定する際、配列やコレクションの要素型とループ変数の型が一致していることが重要です。

C#は型安全な言語であるため、型の不一致があるとコンパイルエラーになります。

ただし、暗黙的型変換が可能な場合は、型の違いがあっても問題なく動作します。

例えば、int型の配列をlong型のループ変数で受け取る場合、intからlongへの暗黙的な型変換があるためエラーになりません。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3 };
        foreach (long number in numbers) // int -> long は暗黙的変換可能
        {
            Console.WriteLine(number);
        }
    }
}
1
2
3

一方で、暗黙的変換がない型同士の場合は、明示的に型変換を行う必要があります。

例えば、object型の配列をstring型のループ変数で受け取ろうとするとコンパイルエラーになります。

object[] objects = { "apple", "banana", "cherry" };
// 以下はコンパイルエラーになる
// foreach (string fruit in objects) { ... }

この場合は、ループ内で明示的にキャストするか、object型のまま扱う必要があります。

foreach (object obj in objects)
{
    string fruit = obj as string;
    if (fruit != null)
    {
        Console.WriteLine(fruit);
    }
}

暗黙的型変換が可能な場合は型安全に処理が行えますが、そうでない場合は明示的なキャストや型チェックを行い、例外やエラーを防ぐことが大切です。

ジェネリック配列との相性

C#のジェネリック型は型安全性を高めるために設計されており、foreach文との相性も非常に良いです。

ジェネリック配列やジェネリックコレクションは、要素の型が明確に指定されているため、ループ変数の型と一致させることで安全に反復処理ができます。

例えば、List<T>IEnumerable<T>を実装しているため、foreach文で簡単に要素を列挙できます。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        List<string> fruits = new List<string> { "りんご", "みかん", "ぶどう" };
        foreach (string fruit in fruits)
        {
            Console.WriteLine(fruit);
        }
    }
}
りんご
みかん
ぶどう

ジェネリック配列も同様に使えます。

int[] numbers = new int[] { 10, 20, 30 };
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
10
20
30

ジェネリック型を使うことで、要素の型が明確になり、foreach文のループ変数の型と一致させることで型安全なコードが書けます。

これにより、実行時の型エラーを防ぎ、コードの信頼性が向上します。

また、ジェネリック型はボックス化やアンボックス化のコストを削減できるため、パフォーマンス面でも有利です。

特に値型の配列やコレクションを扱う場合は、ジェネリックを活用することが推奨されます。

スコープと読み取り専用性

ループ変数のスコープ

foreach文のループ変数は、ループの本体内でのみ有効なローカル変数として扱われます。

つまり、ループの外側からはアクセスできません。

これにより、ループ変数のスコープが限定され、意図しない変数の衝突や誤用を防げます。

以下の例を見てみましょう。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3 };
        foreach (int number in numbers)
        {
            Console.WriteLine(number); // ループ内でnumberを使用可能
        }
        // Console.WriteLine(number); // コンパイルエラー: numberはスコープ外
    }
}
1
2
3

上記のコードでは、foreachのループ変数numberはループの外側では認識されません。

もしループ外でnumberを参照しようとするとコンパイルエラーになります。

このスコープの限定は、ループ変数がループごとに新しく生成されることを意味し、ループ内での変数の状態が外部に影響を与えない安全な設計です。

要素の変更可否と readonly 参照

foreach文のループ変数は、基本的に読み取り専用readonlyとして扱われます。

これは、ループ変数を通じて配列やコレクションの要素を直接変更できないことを意味します。

例えば、以下のコードはコンパイルエラーになります。

int[] numbers = { 1, 2, 3 };
foreach (int number in numbers)
{
    number = number + 1; // コンパイルエラー: ループ変数は読み取り専用
}

この制約は、foreach文が要素の安全な読み取りに特化しているためです。

配列やコレクションの要素を変更したい場合は、for文を使ってインデックス経由でアクセスする必要があります。

for (int i = 0; i < numbers.Length; i++)
{
    numbers[i] = numbers[i] + 1; // 要素の変更が可能
}

パフォーマンスの視点

JIT最適化の影響

C#のforeach文は、JIT(Just-In-Time)コンパイラによって最適化されるため、配列やコレクションの走査において高いパフォーマンスを発揮します。

特に配列に対するforeachは、JITが内部的に単純なインデックスアクセスに変換することが多く、ほぼfor文と同等の速度で動作します。

JIT最適化のポイントは以下の通りです。

  • 配列の走査は高速化される

配列は連続したメモリ領域に格納されているため、JITはforeachのループ変数を使った走査を単純なポインタ操作やインデックスアクセスに変換し、高速に処理します。

  • コレクションの種類によって最適化の度合いが異なる

リストや他のコレクションでは、foreachIEnumeratorを使った列挙処理になるため、配列ほど高速ではありません。

ただし、List<T>のような一般的なコレクションでもJITは最適化を行い、十分なパフォーマンスを確保しています。

  • 不要なオブジェクト生成を抑制

JITはforeachの列挙子の生成や破棄を効率化し、ガベージコレクションの負荷を軽減します。

このように、JIT最適化によりforeach文はパフォーマンス面でも優れた選択肢となっています。

for との速度比較

foreach文とfor文はどちらも配列やコレクションの要素を走査するために使われますが、パフォーマンス面での違いが気になることがあります。

ここでは、小規模配列と大規模配列での速度比較を見ていきます。

小規模配列

小規模な配列(数十~数百要素程度)では、foreachforの速度差はほとんど無視できるレベルです。

JIT最適化により、foreachは内部的にforと同様の処理に変換されるため、実行時間に大きな差は出ません。

以下は簡単なベンチマーク例です。

using System;
using System.Diagnostics;
class Program
{
    static void Main()
    {
        int[] numbers = new int[100];
        for (int i = 0; i < numbers.Length; i++)
            numbers[i] = i;
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int sum1 = 0;
        foreach (int n in numbers)
            sum1 += n;
        sw.Stop();
        Console.WriteLine($"foreach: {sw.ElapsedTicks} ticks");
        sw.Restart();
        int sum2 = 0;
        for (int i = 0; i < numbers.Length; i++)
            sum2 += numbers[i];
        sw.Stop();
        Console.WriteLine($"for: {sw.ElapsedTicks} ticks");
    }
}
foreach: 6 ticks
for: 1 ticks

実行結果は環境によって異なりますが、ほぼ同じかforがわずかに速い程度です。

大規模配列

大規模な配列(数百万要素以上)でも、foreachforのパフォーマンス差は非常に小さいです。

JIT最適化が効いているため、どちらを使っても高速に処理できます。

ただし、極限までパフォーマンスを追求する場合は、for文の方がわずかに有利なケースがあります。

また、for文はインデックスを使うため、ループの途中で要素のスキップや逆順処理など柔軟な制御が可能です。

foreachは単純な順次走査に特化しているため、こうした制御が必要な場合はforを選ぶと良いでしょう。

Unityなど特殊環境での注意点

Unityのようなゲーム開発環境では、foreach文の使用に注意が必要な場合があります。

特に、foreachが内部で列挙子を生成するコレクションに対して使うと、GC(ガベージコレクション)が頻繁に発生し、パフォーマンス低下の原因になることがあります。

  • 配列に対するforeachは問題なし

配列は列挙子を生成しないため、GC発生の心配はほとんどありません。

Unityでも安心して使えます。

  • List<T>などのコレクションは列挙子生成に注意

List<T>foreachは列挙子を生成するため、頻繁に使うとGC負荷が増えます。

特にフレーム単位で繰り返し呼ばれる処理では注意が必要です。

  • 代替手段としてfor文の利用

GC発生を抑えたい場合は、for文でインデックスを使って走査する方法が推奨されます。

これにより、不要なオブジェクト生成を防げます。

  • UnityのBurstコンパイラやJobsシステムとの相性

UnityのBurstコンパイラやJobsシステムでは、foreachの使用が制限されることがあります。

これらの環境では、for文や専用のAPIを使うことが推奨されます。

まとめると、Unityなどの特殊環境ではforeachの使い方に注意し、配列に対しては問題なく使えますが、他のコレクションではパフォーマンス面の影響を考慮して使い分けることが重要です。

多次元配列の処理

二次元配列

C#の二次元配列は、行と列の2つの次元を持つ固定サイズの配列です。

foreach文を使って二次元配列の全要素を順に処理する場合は、単一のforeachで全要素を走査するか、ネストしたforeachで行ごとに処理する方法があります。

以下は二次元配列を単一のforeachで走査する例です。

using System;
class Program
{
    static void Main()
    {
        int[,] matrix = {
            { 1, 2, 3 },
            { 4, 5, 6 },
            { 7, 8, 9 }
        };
        foreach (int value in matrix)
        {
            Console.Write(value + " ");
        }
    }
}
1 2 3 4 5 6 7 8 9

このように、二次元配列は内部的に連続したメモリとして扱われているため、単一のforeachで全要素を順に走査できます。

次に、行ごとに処理したい場合はネストしたforeachを使います。

using System;
class Program
{
    static void Main()
    {
        int[,] matrix = {
            { 1, 2, 3 },
            { 4, 5, 6 },
            { 7, 8, 9 }
        };
        int rows = matrix.GetLength(0);
        int cols = matrix.GetLength(1);
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < cols; j++)
            {
                Console.Write(matrix[i, j] + " ");
            }
            Console.WriteLine();
        }
    }
}
1 2 3
4 5 6
7 8 9

二次元配列はforeachで直接ネストして走査することはできません。

foreachは配列の要素を順に取り出すため、int[,]の要素はintであり、foreachの中でさらにforeachを使うことはできません。

そのため、行ごとに処理したい場合はfor文を使うのが一般的です。

ジャグ配列

ジャグ配列(配列の配列)は、各行の長さが異なる可変長の多次元配列です。

string[][]のように宣言し、各要素が別の配列を指します。

foreach文を使うと、ネストしたループで簡単に全要素を処理できます。

以下はジャグ配列の例です。

using System;
class Program
{
    static void Main()
    {
        string[][] teams = {
            new string[] { "勇者", "戦士", "魔法使い" },
            new string[] { "盗賊", "忍者" },
            new string[] { "スライム", "ドラゴン", "魔王", "ゴーレム" }
        };
        foreach (string[] team in teams)
        {
            foreach (string member in team)
            {
                Console.Write(member + " ");
            }
            Console.WriteLine();
        }
    }
}
勇者 戦士 魔法使い
盗賊 忍者
スライム ドラゴン 魔王 ゴーレム

ジャグ配列は各行の長さが異なるため、foreachで各行の配列を取り出し、さらに内側のforeachでその行の要素を処理します。

これにより、可変長の多次元データを柔軟に扱えます。

ネスト foreach の落とし穴

ネストしたforeach文を使う際には、いくつかの注意点があります。

  1. 二次元配列での誤用

二次元配列(int[,]など)に対して、外側と内側の両方でforeachを使うことはできません。

二次元配列の要素は単一の値型であり、内側のforeachでさらに列挙できる要素がないためです。

誤ってネストforeachを使うとコンパイルエラーになります。

  1. パフォーマンスへの影響

ネストしたforeachは、特に大きなジャグ配列や多次元配列で使うと、ループの回数が増えパフォーマンスに影響を与えることがあります。

必要に応じてfor文に切り替えたり、ループの最適化を検討してください。

  1. ループ変数のスコープに注意

ネストしたforeachでは、内側と外側のループ変数名が重複しないように注意しましょう。

変数名が同じだと、内側の変数が外側の変数を隠蔽し、意図しない動作を招くことがあります。

  1. コレクションの変更禁止

foreach中にコレクションを変更するとInvalidOperationExceptionが発生します。

ネストしたforeachでも同様で、ループ中に配列やリストの構造を変更しないようにしてください。

これらの点に注意しながら、ネストしたforeachを使うことで多次元配列やジャグ配列の要素を安全かつ効率的に処理できます。

制御フロー

break と continue

foreach文の中でループの制御を行う際に使う代表的なキーワードがbreakcontinueです。

  • break

ループを即座に終了させ、foreach文の外側の処理に制御を移します。

特定の条件を満たしたらループを抜けたい場合に使います。

  • continue

ループの現在の反復処理をスキップし、次の要素の処理に移ります。

特定の条件で処理を飛ばしたい場合に使います。

以下の例では、配列の中で値が3になったらループを終了し、値が2のときは処理をスキップしています。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        foreach (int number in numbers)
        {
            if (number == 2)
            {
                Console.WriteLine("2はスキップします");
                continue; // 2のときは処理をスキップ
            }
            if (number == 3)
            {
                Console.WriteLine("3でループを終了します");
                break; // 3のときはループを終了
            }
            Console.WriteLine(number);
        }
    }
}
1
2はスキップします
3でループを終了します

このようにbreakcontinueを使うことで、foreachのループ処理を柔軟に制御できます。

return との違い

returnはメソッドの実行を終了し、呼び出し元に制御を戻すキーワードです。

foreach文の中でreturnを使うと、ループだけでなくメソッド全体の処理が終了します。

以下の例で違いを確認しましょう。

using System;
class Program
{
    static void Main()
    {
        PrintNumbers();
        Console.WriteLine("Mainメソッドの処理続行");
    }
    static void PrintNumbers()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        foreach (int number in numbers)
        {
            if (number == 3)
            {
                Console.WriteLine("3でメソッドを終了します");
                return; // メソッド全体を終了
            }
            Console.WriteLine(number);
        }
        Console.WriteLine("PrintNumbersメソッドの最後まで到達");
    }
}
1
2
3でメソッドを終了します
Mainメソッドの処理続行

この例では、numberが3になった時点でreturnが実行され、PrintNumbersメソッドの処理が終了します。

foreachループもそこで終了し、Mainメソッドに制御が戻ります。

一方、breakはループだけを終了し、メソッドの処理は続行されます。

continueはループの現在の反復をスキップし、次の反復に進みます。

goto case の使用例

goto caseswitch文の中で特定のcaseラベルにジャンプするための制御文です。

foreach文の中でswitchを使う場合、条件によって別のcaseに処理を移したいときに利用できます。

以下はforeach内のswitchgoto caseを使った例です。

using System;
class Program
{
    static void Main()
    {
        string[] fruits = { "りんご", "みかん", "バナナ", "ぶどう" };
        foreach (string fruit in fruits)
        {
            switch (fruit)
            {
                case "りんご":
                    Console.WriteLine("赤い果物です");
                    break;
                case "みかん":
                    Console.WriteLine("オレンジ色の果物です");
                    break;
                case "バナナ":
                    Console.WriteLine("黄色い果物です");
                    goto case "ぶどう"; // ぶどうの処理にジャンプ
                case "ぶどう":
                    Console.WriteLine("小さな果物です");
                    break;
                default:
                    Console.WriteLine("不明な果物です");
                    break;
            }
        }
    }
}
赤い果物です
オレンジ色の果物です
黄色い果物です
小さな果物です
小さな果物です

この例では、バナナのケースでgoto case "ぶどう"を使い、ぶどうの処理にジャンプしています。

結果としてバナナの処理とぶどうの処理が連続して実行されます。

goto caseはコードの重複を避けたい場合や、複数のケースで共通の処理を行いたい場合に便利です。

ただし、乱用するとコードの可読性が低下するため、適切に使うことが重要です。

例外とエラーハンドリング

NullReferenceException を防ぐ

foreach文を使う際に最もよく遭遇する例外の一つがNullReferenceExceptionです。

これは、foreachの対象となる配列やコレクションがnullの場合に発生します。

nullのオブジェクトに対して要素を列挙しようとすると、実行時に例外がスローされます。

以下のコードはnullの配列に対してforeachを実行しようとしているため、NullReferenceExceptionが発生します。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = null;
        foreach (int number in numbers) // NullReferenceExceptionが発生
        {
            Console.WriteLine(number);
        }
    }
}

この例外を防ぐためには、foreachを実行する前に対象の配列やコレクションがnullでないことを必ず確認することが重要です。

nullチェックを行うことで安全にループ処理ができます。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = null;
        if (numbers != null)
        {
            foreach (int number in numbers)
            {
                Console.WriteLine(number);
            }
        }
        else
        {
            Console.WriteLine("配列がnullです。処理をスキップします。");
        }
    }
}
配列がnullです。処理をスキップします。

また、C# 6.0以降では、null条件演算子?.を使って簡潔に書くことも可能です。

ただし、foreach文自体はnull条件演算子に対応していないため、以下のようにIEnumerableを取得してからforeachを使う方法が一般的です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = null;
        foreach (int number in numbers ?? Enumerable.Empty<int>())
        {
            Console.WriteLine(number);
        }
    }
}

この方法では、numbersnullの場合は空の列挙子を使うため、例外が発生せず安全に処理できます。

InvalidOperationException の原因

InvalidOperationExceptionは、foreach文でコレクションを列挙している最中に、そのコレクションの構造が変更された場合に発生します。

具体的には、List<T>Dictionary<TKey, TValue>などのコレクションに対してforeachを実行中に、要素の追加・削除・クリアなどの操作を行うと例外がスローされます。

以下の例では、foreachのループ内でリストに要素を追加しているため、InvalidOperationExceptionが発生します。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3 };
        foreach (int number in numbers)
        {
            Console.WriteLine(number);
            if (number == 2)
            {
                numbers.Add(4); // ループ中にコレクションを変更 → 例外発生
            }
        }
    }
}

この例外を防ぐには、foreachのループ中にコレクションを変更しないことが基本です。

もし要素の追加や削除が必要な場合は、以下のような方法を検討してください。

  • ループ前に変更を完了させる

ループを開始する前に必要な要素の追加や削除を済ませておく。

  • 別のコレクションに変更内容を記録し、ループ後に反映する

ループ中は変更を控え、変更したい要素を別のリストなどに保存し、ループ終了後にまとめて処理します。

  • for文を使う

インデックスを使ったfor文であれば、要素の追加や削除を慎重に行いながら処理できる場合があります。

ただし、要素数の変化に注意が必要です。

  • ToList()でコピーを作成してループする

コレクションのコピーを作成し、そのコピーに対してforeachを行うことで、元のコレクションの変更を回避できます。

foreach (int number in numbers.ToList())
{
    if (number == 2)
    {
        numbers.Add(4); // コピーに対してループしているため例外は発生しない
    }
}

このように、InvalidOperationExceptionはコレクションの安全な列挙を妨げるため、foreach中のコレクション変更は避けることが重要です。

安全なコードを書くために、コレクションの状態管理に注意しましょう。

null 許容参照型(NRT)との併用

前提知識

C# 8.0から導入されたnull許容参照型(Nullable Reference Types、略してNRT)は、参照型変数がnullを許容するかどうかを型システムで明示的に区別できる機能です。

これにより、null参照による実行時エラーをコンパイル時に検出しやすくなります。

NRTが有効な状態では、通常の参照型はnullを許容しない型として扱われ、nullを代入しようとすると警告が発生します。

一方、string?MyClass?のように型名の後ろに?を付けることで、その変数はnullを許容する参照型として宣言されます。

例えば、以下のように宣言します。

string nonNullable = "Hello"; // null不可
string? nullable = null;      // null許容

この機能は、foreach文での配列やコレクションの走査時にも役立ちます。

特に、nullが混入する可能性のあるコレクションを扱う場合、NRTを活用して安全にコードを書くことができます。

nullチェックパターン

NRTを使う場合、foreach文でnull許容型のコレクションを扱う際には、nullチェックを適切に行うことが重要です。

これにより、NullReferenceExceptionの発生を防ぎ、安全に要素を処理できます。

以下は、string?[]型の配列をforeachで走査し、nullの要素をスキップする例です。

using System;
class Program
{
    static void Main()
    {
        string?[] names = { "Alice", null, "Bob", null, "Charlie" };
        foreach (string? name in names)
        {
            if (name is null)
            {
                Console.WriteLine("nullの要素をスキップします");
                continue; // nullの場合は処理をスキップ
            }
            Console.WriteLine(name);
        }
    }
}
Alice
nullの要素をスキップします
Bob
nullの要素をスキップします
Charlie

このように、null許容型の要素を扱う場合は、is null== nullで明示的にnullチェックを行い、nullの要素を適切に処理またはスキップすることが推奨されます。

また、nullチェックを行った後は、コンパイラがその変数が非nullであることを認識するため、以降のコードで安全にメンバーアクセスやメソッド呼び出しが可能です。

別のパターンとして、null合体演算子??を使ってデフォルト値を設定する方法もあります。

foreach (string? name in names)
{
    string safeName = name ?? "名前不明";
    Console.WriteLine(safeName);
}
Alice
名前不明
Bob
名前不明
Charlie

この方法では、nullの要素に対してデフォルトの文字列を割り当てて処理を続行できます。

さらに、NRTを有効にしている場合、コレクション自体がnullの可能性がある場合は、foreachの前にコレクションのnullチェックも忘れずに行いましょう。

string?[]? names = null;
if (names != null)
{
    foreach (string? name in names)
    {
        // nullチェック処理
    }
}

このように、NRTとforeachを組み合わせる際は、コレクションと要素の両方に対してnullチェックを適切に行い、安全なコードを書くことが重要です。

拡張機能との連携

LINQ と foreach の相互運用

LINQ(Language Integrated Query)は、C#でコレクションや配列に対してクエリ操作を簡潔に記述できる機能です。

LINQのメソッドはIEnumerable<T>を返すため、foreach文と非常に相性が良く、LINQで絞り込んだ結果をforeachで順に処理するパターンがよく使われます。

以下は、LINQのWhereメソッドで偶数だけを抽出し、foreachで表示する例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6 };
        var evenNumbers = numbers.Where(n => n % 2 == 0);
        foreach (int number in evenNumbers)
        {
            Console.WriteLine(number);
        }
    }
}
2
4
6

この例では、Whereが遅延実行されるため、foreachが実行されるタイミングで初めてフィルタリング処理が行われます。

LINQのクエリ結果はIEnumerable<T>であるため、foreachで簡単に列挙可能です。

また、LINQのSelectOrderByなどのメソッドも同様にforeachと組み合わせて使えます。

これにより、複雑なデータ変換や並べ替えを簡潔に記述しつつ、foreachで結果を処理できます。

Span<T> との組み合わせ

Span<T>は、C# 7.2以降で導入された構造体で、連続したメモリ領域を安全かつ効率的に扱うための型です。

Span<T>は配列や部分配列、アンマネージドメモリなどをラップし、コピーを伴わずに高速にアクセスできます。

Span<T>foreach文で列挙可能であり、配列と同様に要素を順に処理できます。

以下はSpan<int>を使った例です。

using System;
class Program
{
    static void Main()
    {
        int[] array = { 10, 20, 30, 40, 50 };
        Span<int> span = array.AsSpan(1, 3); // 20, 30, 40 の部分スパン
        foreach (int value in span)
        {
            Console.WriteLine(value);
        }
    }
}
20
30
40

Span<T>はスタック上に割り当てられるため、GCの負荷を減らし、パフォーマンス向上に寄与します。

foreachでの走査も高速で、配列の部分範囲を効率的に処理したい場合に便利です。

ただし、Span<T>は構造体であり、foreachのループ変数はコピーされるため、refを使った変更はできません。

要素の変更が必要な場合は、インデックスアクセスを使うか、ref foreach(C# 7.3以降)を検討してください。

値ツリーの foreach

値ツリー(Value Trees)とは、ツリー構造を持つデータを値型(構造体)で表現したものです。

ツリーの各ノードが値型であるため、ヒープ割り当てを減らし、パフォーマンスとメモリ効率を向上させることができます。

値ツリーの走査にはforeachがよく使われますが、通常の参照型ツリーと異なり、値型の列挙子を実装する必要があります。

これにより、ボックス化を防ぎつつ安全に列挙できます。

以下は、簡単な値ツリー構造とforeachでの走査例です。

using System;
using System.Collections;
using System.Collections.Generic;
struct TreeNode : IEnumerable<TreeNode>
{
    public int Value;
    public TreeNode[] Children;
    public TreeNode(int value, TreeNode[] children)
    {
        Value = value;
        Children = children;
    }
    public IEnumerator<TreeNode> GetEnumerator()
    {
        foreach (var child in Children)
        {
            yield return child;
        }
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
class Program
{
    static void Main()
    {
        var leaf1 = new TreeNode(3, Array.Empty<TreeNode>());
        var leaf2 = new TreeNode(4, Array.Empty<TreeNode>());
        var root = new TreeNode(1, new[] { leaf1, leaf2 });
        foreach (var child in root)
        {
            Console.WriteLine(child.Value);
        }
    }
}
3
4

この例では、TreeNodeが構造体であり、IEnumerable<TreeNode>を実装してforeachで子ノードを列挙しています。

値型の列挙子を使うことで、ボックス化を避けつつツリー構造を効率的に走査できます。

値ツリーのforeachは、パフォーマンスが重要なシナリオや大量のツリー構造を扱う場合に特に有効です。

適切に実装することで、メモリ割り当てを抑えつつ安全に列挙処理が可能になります。

async/await と非同期列挙

IAsyncEnumerable<T> の基礎

C# 8.0から導入されたIAsyncEnumerable<T>は、非同期に要素を列挙できるインターフェースです。

従来のIEnumerable<T>は同期的に要素を返しますが、IAsyncEnumerable<T>は非同期処理を伴うデータのストリームを扱うのに適しています。

IAsyncEnumerable<T>は、非同期にデータを逐次取得したい場合や、ネットワーク通信やファイル読み込みなどの遅延が発生する処理で特に有効です。

これにより、データが準備でき次第順次処理を進められ、UIの応答性やリソース効率が向上します。

IAsyncEnumerable<T>IAsyncEnumerator<T>を返すGetAsyncEnumeratorメソッドを持ち、MoveNextAsyncメソッドで非同期に次の要素を取得します。

これらはawaitと組み合わせて使われます。

以下はIAsyncEnumerable<int>を返す非同期メソッドの例です。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
    static async IAsyncEnumerable<int> GenerateNumbersAsync()
    {
        for (int i = 1; i <= 5; i++)
        {
            await Task.Delay(500); // 0.5秒待機(非同期処理の例)
            yield return i;
        }
    }
    static async Task Main()
    {
        await foreach (var number in GenerateNumbersAsync())
        {
            Console.WriteLine(number);
        }
    }
}

この例では、GenerateNumbersAsyncが非同期に1から5までの数字を生成し、await Task.Delayで遅延をシミュレートしています。

IAsyncEnumerable<T>を使うことで、非同期に要素を逐次取得できます。

await foreach の構文

await foreachは、IAsyncEnumerable<T>を非同期に列挙するための構文です。

通常のforeachは同期的に要素を列挙しますが、await foreachは非同期に要素を取得しながらループを進めます。

await foreachの基本構文は以下の通りです。

await foreach (var item in asyncEnumerable)
{
    // 非同期に取得した要素を処理
}

await foreachは、IAsyncEnumerable<T>GetAsyncEnumeratorメソッドを呼び出し、MoveNextAsyncawaitしながら次の要素を取得します。

これにより、非同期処理の完了を待ってからループを継続できます。

以下はawait foreachを使った具体例です。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
    static async IAsyncEnumerable<string> GetMessagesAsync()
    {
        string[] messages = { "Hello", "World", "Async", "Foreach" };
        foreach (var msg in messages)
        {
            await Task.Delay(300); // 非同期遅延
            yield return msg;
        }
    }
    static async Task Main()
    {
        await foreach (var message in GetMessagesAsync())
        {
            Console.WriteLine(message);
        }
    }
}
Hello
World
Async
Foreach

このコードでは、GetMessagesAsyncが非同期に文字列を返し、await foreachで順に受け取って表示しています。

await foreachを使うことで、非同期ストリームの処理が簡潔かつ直感的に書けます。

await foreachは非同期列挙の完了まで待機し、例外も適切に伝播するため、エラーハンドリングも通常のtry-catchで対応可能です。

まとめると、IAsyncEnumerable<T>await foreachは、非同期ストリーム処理をC#で自然に扱うための強力な機能であり、非同期プログラミングの表現力と安全性を大きく向上させます。

カスタム列挙子

IEnumerable と IEnumerator の実装

C#のforeach文は、対象のオブジェクトがIEnumerableまたはIEnumerable<T>インターフェースを実装していることを前提に動作します。

これらのインターフェースは、列挙子(Enumerator)を返すGetEnumeratorメソッドを持ち、列挙子はIEnumeratorまたはIEnumerator<T>を実装しています。

カスタムコレクションでforeachを使いたい場合は、これらのインターフェースを実装する必要があります。

以下は、IEnumerable<int>IEnumerator<int>を自作して整数の範囲を列挙する例です。

using System;
using System.Collections;
using System.Collections.Generic;
class Range : IEnumerable<int>
{
    private readonly int _start;
    private readonly int _count;
    public Range(int start, int count)
    {
        _start = start;
        _count = count;
    }
    public IEnumerator<int> GetEnumerator()
    {
        return new RangeEnumerator(_start, _count);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    private class RangeEnumerator : IEnumerator<int>
    {
        private readonly int _start;
        private readonly int _count;
        private int _currentIndex;
        public RangeEnumerator(int start, int count)
        {
            _start = start;
            _count = count;
            _currentIndex = -1;
        }
        public int Current => _start + _currentIndex;
        object IEnumerator.Current => Current;
        public bool MoveNext()
        {
            _currentIndex++;
            return _currentIndex < _count;
        }
        public void Reset()
        {
            _currentIndex = -1;
        }
        public void Dispose()
        {
            // リソース解放不要なので空実装
        }
    }
}
class Program
{
    static void Main()
    {
        var range = new Range(5, 4); // 5,6,7,8
        foreach (int number in range)
        {
            Console.WriteLine(number);
        }
    }
}
5
6
7
8

この例では、RangeクラスがIEnumerable<int>を実装し、RangeEnumeratorIEnumerator<int>を実装しています。

foreachGetEnumeratorで取得した列挙子のMoveNextCurrentを使って要素を順に取得します。

yield return を使った独自配列走査

yield returnは、C#のイテレータ機能で、列挙子の実装を簡潔に書ける構文です。

yield returnを使うと、状態を保持するステートマシンがコンパイラによって自動生成され、手動でIEnumeratorを実装する必要がなくなります。

以下は、先ほどのRangeクラスをyield returnで書き換えた例です。

using System;
using System.Collections.Generic;
class Range : IEnumerable<int>
{
    private readonly int _start;
    private readonly int _count;
    public Range(int start, int count)
    {
        _start = start;
        _count = count;
    }
    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i < _count; i++)
        {
            yield return _start + i;
        }
    }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
class Program
{
    static void Main()
    {
        var range = new Range(10, 3); // 10,11,12
        foreach (int number in range)
        {
            Console.WriteLine(number);
        }
    }
}
10
11
12

遅延実行のメリット

yield returnを使った列挙は遅延実行(Lazy Evaluation)となります。

つまり、foreachで要素が要求されるまで、実際の処理は行われません。

これにより、以下のメリットがあります。

  • メモリ効率の向上

全要素を一度に生成・保持する必要がなく、必要な分だけ順次生成できるためメモリ使用量が抑えられます。

  • パフォーマンスの最適化

必要な要素だけを処理するため、無駄な計算や処理を避けられます。

  • 無限列挙の実装が可能

無限に続くシーケンスもyield returnで簡単に表現でき、必要な分だけ列挙できます。

ステートマシン生成

yield returnを使うと、コンパイラは内部的に状態を管理するステートマシンを自動生成します。

このステートマシンは、IEnumeratorMoveNextCurrentの動作を実装し、列挙の状態を保持します。

この仕組みにより、開発者は複雑な列挙子の状態管理を意識せずに、簡潔なコードでカスタム列挙を実装できます。

ただし、ステートマシンの生成により若干のオーバーヘッドが発生するため、パフォーマンスが極めて重要な場面では手動実装が検討されることもあります。

このように、IEnumerable/IEnumeratorの実装やyield returnを活用することで、独自のコレクションや配列走査を簡単かつ効率的に実装できます。

foreach文と組み合わせて使うことで、直感的で安全な反復処理が可能です。

ref foreach(C# 7.3以降)

参照戻り値の仕組み

C# 7.3から導入されたref foreachは、配列やコレクションの要素を「参照」でループ変数に渡すことができる構文です。

通常のforeachではループ変数は値のコピーとして扱われるため、要素の直接変更はできませんが、ref foreachを使うと配列の要素自体を参照し、ループ内で直接書き換えが可能になります。

この仕組みの根底には「参照戻り値(ref return)」があります。

参照戻り値とは、メソッドやインデクサーが値のコピーではなく、変数の実体そのものへの参照を返す機能です。

これにより、呼び出し元で返された参照を通じて元のデータを直接操作できます。

ref foreachは、refを使ってループ変数を参照型として宣言し、配列やコレクションの要素を参照戻り値として取得します。

これにより、ループ内での要素の読み書きが可能になります。

以下はref foreachの基本的な例です。

using System;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        // ref foreachで配列の要素を参照として取得し、直接変更可能
        foreach (ref int number in numbers)
        {
            number *= 2; // 元の配列の要素を直接変更
        }
        foreach (int number in numbers)
        {
            Console.WriteLine(number);
        }
    }
}
2
4
6
8
10

この例では、ref int numberが配列numbersの各要素を参照として受け取り、number *= 2で元の配列の値を直接変更しています。

通常のforeachではこのような書き換えはできません。

性能と安全性のバランス

ref foreachは性能面で大きなメリットがあります。

通常のforeachではループ変数が値のコピーであるため、特に構造体などの大きな値型を扱う場合、コピーコストが無視できません。

ref foreachを使うことでコピーを避け、直接参照を操作するため、メモリ効率と処理速度が向上します。

また、配列の要素を直接書き換えられるため、余計なインデックスアクセスや一時変数の使用を減らせます。

これにより、パフォーマンスが求められる場面で有効です。

一方で、安全性の面でも配慮が必要です。

参照を直接操作するため、誤って配列の要素を不正に変更したり、参照の寿命を超えてアクセスしたりすると、バグや予期せぬ動作の原因になります。

特に、ref foreachは配列や特定のコレクションでのみ利用可能であり、すべてのコレクションで使えるわけではありません。

また、ref foreachのループ変数は読み取り専用ではなく書き込み可能なため、ループ内での変更が元のデータに即座に反映されます。

これを理解していないと、意図しない副作用が発生するリスクがあります。

まとめると、ref foreachは性能向上とメモリ効率の改善に寄与する強力な機能ですが、参照操作の特性を理解し、安全に使うことが重要です。

適切に使えば、特に大きな値型配列の処理や頻繁な要素更新が必要な場面で効果を発揮します。

破棄可能型配列の列挙

using foreach パターン

C#では、IDisposableインターフェースを実装したオブジェクトは、使用後にDisposeメソッドを呼び出してリソースを解放する必要があります。

foreach文でIDisposableを実装したオブジェクトの配列やコレクションを列挙する場合、各要素の破棄処理を適切に行うことが重要です。

一般的に、foreach文自体は列挙子(Enumerator)がIDisposableを実装していれば、ループ終了時に自動的にDisposeを呼び出しますが、配列の要素がIDisposableの場合は自動的に破棄されません。

そのため、配列の各要素を明示的に破棄する必要があります。

using文とforeachを組み合わせて、各要素の破棄を安全かつ簡潔に行うパターンを紹介します。

using System;
class DisposableResource : IDisposable
{
    private readonly string _name;
    public DisposableResource(string name)
    {
        _name = name;
        Console.WriteLine($"{_name} を作成しました");
    }
    public void Use()
    {
        Console.WriteLine($"{_name} を使用中");
    }
    public void Dispose()
    {
        Console.WriteLine($"{_name} を破棄しました");
    }
}
class Program
{
    static void Main()
    {
        var resources = new DisposableResource[]
        {
            new DisposableResource("リソース1"),
            new DisposableResource("リソース2"),
            new DisposableResource("リソース3")
        };
        foreach (var resource in resources)
        {
            using (resource)
            {
                resource.Use();
            }
        }
    }
}
リソース1 を作成しました
リソース2 を作成しました
リソース3 を作成しました
リソース1 を使用中
リソース1 を破棄しました
リソース2 を使用中
リソース2 を破棄しました
リソース3 を使用中
リソース3 を破棄しました

この例では、配列の各要素をforeachで取り出し、using文で囲むことで、要素の使用後に自動的にDisposeが呼ばれます。

これにより、リソースの確実な解放が保証されます。

IDisposable 実装時の注意点

IDisposableを実装した型の配列をforeachで列挙する際には、以下の点に注意が必要です。

  1. 配列の要素は自動的に破棄されない

foreach文は列挙子のDisposeを呼びますが、配列の要素自体がIDisposableでも自動的にDisposeは呼ばれません。

したがって、要素ごとに明示的にDisposeを呼ぶ必要があります。

  1. using文のスコープに注意

using文はスコープを限定してリソースを解放します。

foreachのループ内でusingを使う場合、ループの各反復ごとにリソースが確実に解放されますが、ループ外でまとめて解放したい場合は別途管理が必要です。

  1. 複数回の破棄呼び出しに注意

Disposeは複数回呼ばれても安全に動作するように設計するのが望ましいですが、実装によっては例外が発生することもあります。

配列の要素を複数回破棄しないように注意してください。

  1. 例外発生時のリソースリーク防止

ループ内で例外が発生した場合でも、using文を使うことで確実にDisposeが呼ばれます。

例外処理を適切に行い、リソースリークを防ぎましょう。

  1. 破棄順序の考慮

依存関係のあるリソースが複数ある場合、破棄の順序が重要になることがあります。

配列の順序に依存する場合は、foreachの順序に注意してください。

  1. 非同期破棄の対応

C# 8.0以降ではIAsyncDisposableが導入され、非同期に破棄処理を行うことが可能です。

非同期破棄が必要な場合はawait usingIAsyncDisposableの実装を検討してください。

これらの注意点を踏まえ、IDisposableを実装した配列の要素をforeachで列挙する際は、using文を活用して安全にリソースを解放することが推奨されます。

適切な破棄処理はメモリリークやリソース枯渇を防ぎ、安定したアプリケーション動作に寄与します。

ベストプラクティス集

読みやすい命名

foreach文を使う際のループ変数や関連する変数の命名は、コードの可読性に大きく影響します。

読みやすい命名を心がけることで、コードの意図が明確になり、保守やレビューがスムーズになります。

  • ループ変数は単数形で意味のある名前を使う

配列やコレクションの要素を表すため、複数形のコレクション名から単数形の名前を付けるのが一般的です。

例えば、fruitsというリストならループ変数はfruitusersならuserとします。

List<string> fruits = new List<string> { "りんご", "みかん" };
foreach (var fruit in fruits)
{
    Console.WriteLine(fruit);
}
  • 型や用途に即した名前を選ぶ

例えば、整数の配列ならnumberindex、カスタムクラスの配列ならそのクラス名を使うなど、型や用途がわかる名前にします。

  • 短すぎず長すぎず適切な長さにする

変数名は短すぎると意味が伝わりにくく、長すぎると冗長になります。

適度な長さで簡潔に意味を表現しましょう。

  • 一貫性を保つ

プロジェクト内で命名規則を統一し、同じ意味の変数は同じ命名パターンを使うことで、コード全体の整合性が保たれます。

コメントとドキュメント

foreach文を含むコードには、適切なコメントやドキュメントを付けることで、他の開発者や将来の自分が理解しやすくなります。

  • 処理の意図や理由をコメントする

単純なループ処理はコメント不要な場合もありますが、特別な条件分岐や例外的な処理がある場合は、その意図をコメントで説明します。

foreach (var user in users)
{
    // 管理者ユーザーのみ処理対象とする
    if (!user.IsAdmin) continue;
    ProcessAdminUser(user);
}
  • 複雑なロジックは分割し、メソッドにまとめる

ループ内の処理が複雑になる場合は、別メソッドに切り出し、そのメソッドにXMLドキュメントコメントを付けると理解しやすくなります。

  • XMLドキュメントコメントでAPI仕様を明示

公開メソッドやクラスにはXMLドキュメントコメントを付け、引数や戻り値の説明、例外情報などを記述しましょう。

これにより、IntelliSenseでの補完や自動生成ドキュメントに役立ちます。

コードアナライザ活用

コードアナライザは、静的解析ツールとしてコードの品質向上に役立ちます。

foreach文に関しても、パフォーマンスや安全性、スタイルの観点から改善点を指摘してくれます。

  • Microsoft.CodeAnalysis(Roslyn)ベースのアナライザ

Visual Studioや.NET SDKに組み込まれているRoslynアナライザは、foreachの使い方に関する警告や提案を提供します。

例えば、不要な変数宣言や型推論の活用、パフォーマンス改善のヒントなどです。

  • StyleCopやReSharperなどのサードパーティツール

これらのツールは命名規則やコードスタイルのチェックに加え、foreachの最適化や安全な使い方を促すルールを持っています。

  • パフォーマンス分析ツールとの連携

プロファイラやパフォーマンス分析ツールを使い、foreachの使用がボトルネックになっていないか確認しましょう。

必要に応じてfor文やref foreachへの切り替えを検討できます。

  • カスタムルールの導入

プロジェクト固有のコーディング規約に基づき、foreachの使い方に関するカスタムルールを作成し、CI/CDパイプラインで自動チェックを行うことも効果的です。

これらのコードアナライザを活用することで、foreach文を含むコードの品質を継続的に改善し、バグやパフォーマンス問題の早期発見につなげられます。

foreach 内の index を取得する方法

foreach文はコレクションの要素を順に取り出すための構文であり、ループ変数として要素そのものを受け取ります。

そのため、foreach内で直接インデックス(要素の位置)を取得する機能はありません。

しかし、インデックスが必要な場合はいくつかの方法があります。

手動でカウンターを用意する

最もシンプルな方法は、ループの外でインデックス用の変数を用意し、ループ内でインクリメントする方法です。

int[] numbers = { 10, 20, 30, 40 };
int index = 0;
foreach (int number in numbers)
{
    Console.WriteLine($"Index: {index}, Value: {number}");
    index++;
}
Index: 0, Value: 10
Index: 1, Value: 20
Index: 2, Value: 30
Index: 3, Value: 40

LINQのSelectを使う

LINQのSelectメソッドは、要素とインデックスの両方を引数に取るオーバーロードがあります。

これを利用して、インデックス付きの列挙を行い、foreachで処理できます。

using System.Linq;
int[] numbers = { 10, 20, 30, 40 };
foreach (var item in numbers.Select((value, index) => new { value, index }))
{
    Console.WriteLine($"Index: {item.index}, Value: {item.value}");
}
Index: 0, Value: 10
Index: 1, Value: 20
Index: 2, Value: 30
Index: 3, Value: 40

この方法はコードが簡潔で、インデックスと値を同時に扱いたい場合に便利です。

for文を使う

インデックスが必要な場合は、for文を使うのが最も自然で効率的です。

配列やリストのインデックスを直接使えるため、インデックスと要素の両方を簡単に扱えます。

int[] numbers = { 10, 20, 30, 40 };
for (int i = 0; i < numbers.Length; i++)
{
    Console.WriteLine($"Index: {i}, Value: {numbers[i]}");
}

配列を書き換えたい場合の選択肢

foreach文のループ変数は読み取り専用であるため、ループ内で配列の要素を直接書き換えることはできません。

配列の要素を変更したい場合は、以下の方法を検討してください。

for文を使う

for文はインデックスを使って配列の要素にアクセスするため、要素の読み書きが自由にできます。

int[] numbers = { 1, 2, 3, 4 };
for (int i = 0; i < numbers.Length; i++)
{
    numbers[i] *= 2; // 要素を2倍に変更
}

ref foreach(C# 7.3以降)

C# 7.3以降では、ref foreachを使って配列の要素を参照としてループ変数に渡し、直接書き換えが可能です。

int[] numbers = { 1, 2, 3, 4 };
foreach (ref int number in numbers)
{
    number *= 2; // 直接要素を書き換え
}

新しい配列を作成する

元の配列を変更せずに新しい配列を作成する方法もあります。

LINQのSelectを使って変換した結果を新しい配列に格納できます。

using System.Linq;
int[] numbers = { 1, 2, 3, 4 };
int[] doubled = numbers.Select(n => n * 2).ToArray();

ループ変数をキャプチャするラムダの罠

foreachのループ変数をラムダ式や匿名メソッド内でキャプチャする際に、意図しない動作が起こることがあります。

これは、ループ変数がループ全体で共有されているため、ラムダが実行される時点で変数の値が変わっていることが原因です。

問題の例

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var actions = new List<Action>();
        int[] numbers = { 1, 2, 3 };
        foreach (int number in numbers)
        {
            actions.Add(() => Console.WriteLine(number));
        }
        foreach (var action in actions)
        {
            action(); // すべて最後のnumberの値を表示してしまう
        }
    }
}
3
3
3

この例では、すべてのラムダが最後のループ変数numberの値(3)を参照してしまい、期待した1, 2, 3の順に表示されません。

解決策

ループ変数の値をローカル変数にコピーしてからラムダに渡すことで、この問題を回避できます。

foreach (int number in numbers)
{
    int captured = number; // コピーを作成
    actions.Add(() => Console.WriteLine(captured));
}

これにより、各ラムダはそれぞれ異なるcaptured変数を参照し、期待通りの動作になります。

これらのFAQは、foreach文を使う際に初心者から中級者までよく直面する疑問や問題点を解決するためのポイントです。

適切な方法を選択し、安全で効率的なコードを書くことが重要です。

参考コード断片

サンプルリポジトリ構成例

C#のforeach文を活用したサンプルコードを管理・共有するためのリポジトリ構成例を示します。

整理された構成により、学習やメンテナンスがしやすくなります。

SampleForeachProject/
├── README.md
├── src/
│   ├── BasicExamples/
│   │   ├── Program.cs          # 基本的なforeachの使い方
│   │   └── StringArrayExample.cs
│   ├── AdvancedExamples/
│   │   ├── RefForeachExample.cs # ref foreachのサンプル
│   │   ├── AsyncForeachExample.cs # await foreachのサンプル
│   │   └── CustomEnumerator.cs  # カスタム列挙子の実装例
│   └── Utilities/
│       └── HelperMethods.cs    # 共通のヘルパーメソッド
├── tests/
│   ├── BasicExamplesTests/
│   │   └── BasicExamplesTests.cs
│   ├── AdvancedExamplesTests/
│   │   └── AsyncForeachTests.cs
│   └── UtilitiesTests/
│       └── HelperMethodsTests.cs
├── SampleForeachProject.sln
└── .gitignore
  • src/ディレクトリに機能別にコードを分け、基本例と応用例を分離しています
  • tests/ディレクトリには各機能に対応した単体テストを配置し、品質を保ちます
  • README.mdにはリポジトリの概要や使い方、ビルド方法を記載します
  • .gitignoreでビルド成果物やIDE固有ファイルを除外します

このような構成により、コードの拡張やチームでの共有がスムーズになります。

テストコードの例

foreach文を含むコードの動作を保証するための単体テスト例を示します。

ここでは、基本的な配列の走査とref foreachを使った要素の書き換えをテストします。

using System;
using Xunit;
public class ForeachTests
{
    [Fact]
    public void Foreach_IteratesAllElements()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        int sum = 0;
        foreach (int number in numbers)
        {
            sum += number;
        }
        Assert.Equal(15, sum);
    }
    [Fact]
    public void RefForeach_ModifiesArrayElements()
    {
        int[] numbers = { 1, 2, 3 };
        foreach (ref int number in numbers)
        {
            number *= 10;
        }
        Assert.Equal(new int[] { 10, 20, 30 }, numbers);
    }
    [Fact]
    public void Foreach_WithNullCollection_ThrowsException()
    {
        int[] numbers = null;
        Assert.Throws<NullReferenceException>(() =>
        {
            foreach (var n in numbers)
            {
                // 何もしない
            }
        });
    }
}
  • Foreach_IteratesAllElementsは、foreachで配列の全要素を正しく走査し、合計値を計算できることを検証しています
  • RefForeach_ModifiesArrayElementsは、ref foreachで配列の要素を直接変更できることをテストしています
  • Foreach_WithNullCollection_ThrowsExceptionは、nullの配列に対してforeachを実行するとNullReferenceExceptionが発生することを確認しています

これらのテストはxUnitを使った例ですが、NUnitMSTestでも同様のテストが可能です。

テストコードを充実させることで、foreach文を含む処理の信頼性を高められます。

まとめ

この記事では、C#のforeach文を安全かつ効率的に使うための基本から応用まで幅広く解説しました。

配列やコレクションの走査方法、型安全性、パフォーマンスのポイント、非同期列挙やカスタム列挙子の実装例、さらにベストプラクティスやよくある疑問への対応策も紹介しています。

これらを理解し活用することで、読みやすく保守性の高いコードを書きつつ、パフォーマンスや安全性も確保できるようになります。

関連記事

Back to top button