【C#】foreachで配列を安全かつ効率的にループする基本と活用テクニック
foreach
は配列の要素を先頭から順に読み取る最も手軽な方法です。
インデックスを意識せず要素にアクセスでき、境界チェックも不要なのでバグを減らせます。
ループ中に要素追加や削除はできないため構造を変えたい場合はfor
が向きます。
読み取り専用ループと捉えると扱いやすいです。
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
と書かずに済みます。
ループ変数number
もvar
で宣言しているため、コンパイラが自動的に型を推論します。
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
のループ変数を使った走査を単純なポインタ操作やインデックスアクセスに変換し、高速に処理します。
- コレクションの種類によって最適化の度合いが異なる
リストや他のコレクションでは、foreach
はIEnumerator
を使った列挙処理になるため、配列ほど高速ではありません。
ただし、List<T>
のような一般的なコレクションでもJITは最適化を行い、十分なパフォーマンスを確保しています。
- 不要なオブジェクト生成を抑制
JITはforeach
の列挙子の生成や破棄を効率化し、ガベージコレクションの負荷を軽減します。
このように、JIT最適化によりforeach
文はパフォーマンス面でも優れた選択肢となっています。
for との速度比較
foreach
文とfor
文はどちらも配列やコレクションの要素を走査するために使われますが、パフォーマンス面での違いが気になることがあります。
ここでは、小規模配列と大規模配列での速度比較を見ていきます。
小規模配列
小規模な配列(数十~数百要素程度)では、foreach
とfor
の速度差はほとんど無視できるレベルです。
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
がわずかに速い程度です。
大規模配列
大規模な配列(数百万要素以上)でも、foreach
とfor
のパフォーマンス差は非常に小さいです。
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
文を使う際には、いくつかの注意点があります。
- 二次元配列での誤用
二次元配列(int[,]
など)に対して、外側と内側の両方でforeach
を使うことはできません。
二次元配列の要素は単一の値型であり、内側のforeach
でさらに列挙できる要素がないためです。
誤ってネストforeach
を使うとコンパイルエラーになります。
- パフォーマンスへの影響
ネストしたforeach
は、特に大きなジャグ配列や多次元配列で使うと、ループの回数が増えパフォーマンスに影響を与えることがあります。
必要に応じてfor
文に切り替えたり、ループの最適化を検討してください。
- ループ変数のスコープに注意
ネストしたforeach
では、内側と外側のループ変数名が重複しないように注意しましょう。
変数名が同じだと、内側の変数が外側の変数を隠蔽し、意図しない動作を招くことがあります。
- コレクションの変更禁止
foreach
中にコレクションを変更するとInvalidOperationException
が発生します。
ネストしたforeach
でも同様で、ループ中に配列やリストの構造を変更しないようにしてください。
これらの点に注意しながら、ネストしたforeach
を使うことで多次元配列やジャグ配列の要素を安全かつ効率的に処理できます。
制御フロー
break と continue
foreach
文の中でループの制御を行う際に使う代表的なキーワードがbreak
とcontinue
です。
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でループを終了します
このようにbreak
とcontinue
を使うことで、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 case
はswitch
文の中で特定のcase
ラベルにジャンプするための制御文です。
foreach
文の中でswitch
を使う場合、条件によって別のcase
に処理を移したいときに利用できます。
以下はforeach
内のswitch
でgoto 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);
}
}
}
この方法では、numbers
がnull
の場合は空の列挙子を使うため、例外が発生せず安全に処理できます。
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のSelect
やOrderBy
などのメソッドも同様に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
メソッドを呼び出し、MoveNextAsync
をawait
しながら次の要素を取得します。
これにより、非同期処理の完了を待ってからループを継続できます。
以下は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>
を実装し、RangeEnumerator
がIEnumerator<int>
を実装しています。
foreach
はGetEnumerator
で取得した列挙子のMoveNext
とCurrent
を使って要素を順に取得します。
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
を使うと、コンパイラは内部的に状態を管理するステートマシンを自動生成します。
このステートマシンは、IEnumerator
のMoveNext
やCurrent
の動作を実装し、列挙の状態を保持します。
この仕組みにより、開発者は複雑な列挙子の状態管理を意識せずに、簡潔なコードでカスタム列挙を実装できます。
ただし、ステートマシンの生成により若干のオーバーヘッドが発生するため、パフォーマンスが極めて重要な場面では手動実装が検討されることもあります。
このように、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
で列挙する際には、以下の点に注意が必要です。
- 配列の要素は自動的に破棄されない
foreach
文は列挙子のDispose
を呼びますが、配列の要素自体がIDisposable
でも自動的にDispose
は呼ばれません。
したがって、要素ごとに明示的にDispose
を呼ぶ必要があります。
using
文のスコープに注意
using
文はスコープを限定してリソースを解放します。
foreach
のループ内でusing
を使う場合、ループの各反復ごとにリソースが確実に解放されますが、ループ外でまとめて解放したい場合は別途管理が必要です。
- 複数回の破棄呼び出しに注意
Dispose
は複数回呼ばれても安全に動作するように設計するのが望ましいですが、実装によっては例外が発生することもあります。
配列の要素を複数回破棄しないように注意してください。
- 例外発生時のリソースリーク防止
ループ内で例外が発生した場合でも、using
文を使うことで確実にDispose
が呼ばれます。
例外処理を適切に行い、リソースリークを防ぎましょう。
- 破棄順序の考慮
依存関係のあるリソースが複数ある場合、破棄の順序が重要になることがあります。
配列の順序に依存する場合は、foreach
の順序に注意してください。
- 非同期破棄の対応
C# 8.0以降ではIAsyncDisposable
が導入され、非同期に破棄処理を行うことが可能です。
非同期破棄が必要な場合はawait using
とIAsyncDisposable
の実装を検討してください。
これらの注意点を踏まえ、IDisposable
を実装した配列の要素をforeach
で列挙する際は、using
文を活用して安全にリソースを解放することが推奨されます。
適切な破棄処理はメモリリークやリソース枯渇を防ぎ、安定したアプリケーション動作に寄与します。
ベストプラクティス集
読みやすい命名
foreach
文を使う際のループ変数や関連する変数の命名は、コードの可読性に大きく影響します。
読みやすい命名を心がけることで、コードの意図が明確になり、保守やレビューがスムーズになります。
- ループ変数は単数形で意味のある名前を使う
配列やコレクションの要素を表すため、複数形のコレクション名から単数形の名前を付けるのが一般的です。
例えば、fruits
というリストならループ変数はfruit
、users
ならuser
とします。
List<string> fruits = new List<string> { "りんご", "みかん" };
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}
- 型や用途に即した名前を選ぶ
例えば、整数の配列ならnumber
やindex
、カスタムクラスの配列ならそのクラス名を使うなど、型や用途がわかる名前にします。
- 短すぎず長すぎず適切な長さにする
変数名は短すぎると意味が伝わりにくく、長すぎると冗長になります。
適度な長さで簡潔に意味を表現しましょう。
- 一貫性を保つ
プロジェクト内で命名規則を統一し、同じ意味の変数は同じ命名パターンを使うことで、コード全体の整合性が保たれます。
コメントとドキュメント
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
を使った例ですが、NUnit
やMSTest
でも同様のテストが可能です。
テストコードを充実させることで、foreach
文を含む処理の信頼性を高められます。
まとめ
この記事では、C#のforeach
文を安全かつ効率的に使うための基本から応用まで幅広く解説しました。
配列やコレクションの走査方法、型安全性、パフォーマンスのポイント、非同期列挙やカスタム列挙子の実装例、さらにベストプラクティスやよくある疑問への対応策も紹介しています。
これらを理解し活用することで、読みやすく保守性の高いコードを書きつつ、パフォーマンスや安全性も確保できるようになります。