Niigata.NET 3.0に参加して、forと比較して、foreachをブラックボックスに感じたので、少し整理します。
Niigata.NET 3.0に参加してきました
手始めに、言語仕様は以下の通り。
8.8.4 foreach ステートメント
foreach ステートメントはコレクションの要素を列挙し、コレクションの要素ごとに埋め込みステートメントを実行します。foreach_statement : 'foreach' '(' local_variable_type identifier 'in' expression ')' embedded_statement ;
foreach ステートメントの type および identifier で、ステートメントの "反復変数" を宣言します。local-variable-type として var 識別子が指定されている場合、スコープ内に var という名前の型がないと、反復変数は "暗黙的に型指定された反復変数" となり、その型は次に示すように foreach ステートメントの要素型となります。反復変数は、読み取り専用のローカル変数に対応し、スコープはその埋め込みステートメント全体です。foreach ステートメントの実行中は、反復変数は、現在実行中の反復のコレクション要素を表します。埋め込みステートメントが、代入または ++ 演算子と 演算子で反復変数を変更しようとしたり、反復変数を ref パラメーターまたは out パラメーターとして渡そうとしたりすると、コンパイル エラーが発生します。
以下では、説明を簡単にするために、IEnumerable、IEnumerator、IEnumerable、および IEnumerator は、System.Collections および System.Collections.Generic 名前空間のそれぞれに対応する型を参照します。
foreach ステートメントのコンパイル時の処理では、最初に式の "コレクション型"、"列挙子型"、および "要素型" が決定されます。この決定は次のように処理されます。
- expression の X の型が配列型の場合、X から IEnumerable インターフェイスへの暗黙的な参照変換があります (System.Array がこのインターフェイスを実装するため)。コレクション型は IEnumerable インターフェイス、列挙子型は IEnumerator インターフェイス、要素型は配列型 X の要素型になります。
- expression の型 X が dynamic である場合は、expression から IEnumerable インターフェイス (6.1.8 を参照) への暗黙的な変換があります。コレクション型は IEnumerable インターフェイス、列挙子型は IEnumerator インターフェイスになります。"要素型" は、local-variable-type として var 識別子が指定されている場合は dynamic、それ以外の場合は object になります。
- それ以外の場合は、型 X に適切な GetEnumerator メソッドがあるかどうかを判定します。
- 型引数のない GetEnumerator 識別子を対象として、型 X でメンバー検索を実行します。メンバー検索で一致が見つからなかった場合、あいまいさが生じた場合、またはメソッド グループではない一致が生じた場合は、後で説明する方法に従って列挙可能インターフェイスを確認します。メンバー検索でメソッド グループ以外の結果が生じた場合や一致が見つからなかった場合は、警告を出すことを推奨します。
- 得られたメソッド グループと空の引数リストを使用して、オーバーロード解決を実行します。オーバーロード解決で、該当するメソッドが見つからない、あいまいさが生じる、または 1 つの最善のメソッドが見つかったものの、そのメソッドが静的であるかパブリックでなかった場合は、後で説明する方法に従って列挙可能インターフェイスを確認します。オーバーロード解決で、あいまいでないパブリックなインスタンス メソッド以外のものが生成されるか、または適用可能なメソッドが生成されない場合は、警告を出すことを推奨します。
- GetEnumerator メソッドの戻り値の型 E が、クラス、構造体、またはインターフェイス型でない場合、エラーが発生し、以降の手順は行われません。
- 型引数のない Current 識別子を対象として、E でメンバー検索が実行されます。メンバー検索で一致が見つからない、エラーが発生する、または結果が読み取りを許可されているパブリック インスタンス プロパティ以外である場合、エラーが発生し、以降の手順は行われません。
- 型引数のない MoveNext 識別子を対象として、E でメンバー検索が実行されます。メンバー検索で一致が見つからない、エラーが発生する、または結果がメソッド グループ以外である場合、エラーが発生し、以降の手順は行われません。
- メソッド グループと空の引数リストを使用して、オーバーロード解決が実行されます。オーバーロード解決で適切なメソッドが見つからない、あいまいさが生じる、または 1 つの最善のメソッドが見つかったものの、そのメソッドが静的であるかパブリックでなかった場合、あるいは戻り値の型が bool でない場合は、エラーが発生し、以降の手順は行われません。
- "コレクション型" は X、"列挙子型" は E、"要素型" は Current プロパティの型になります。
- それ以外の場合は、列挙可能なインターフェイスを確認します。
- X から IEnumerable への暗黙の変換がある、すべての型 Ti の中に、一意の型 T があり、T が dynamic ではなく、その他のすべての Ti に対して、IEnumerable から IEnumerable への暗黙の変換がある場合、"コレクション型" がインターフェイス IEnumerable となり、"列挙子型" はインターフェイス IEnumerator となり、"要素型" は T となります。
- それ以外の場合、そのような型 T が複数存在すると、エラーが発生し、以降の手順は行われません。
- それ以外の場合、X から System.Collections.IEnumerable インターフェイスへの暗黙的な変換がある場合、コレクション型はこのインターフェイス、列挙子型はインターフェイス System.Collections.IEnumerator、要素型は object になります。
- それ以外の場合にはエラーが発生し、以降の手順は行われません。
上記の手順が成功すると、コレクション型 C、列挙子型 E、要素型 T が明確に生成されます。foreach ステートメントは次の形式になります。
foreach (V v in x) embedded-statement
これは次のように展開されます。
{ E e = ((C)(x)).GetEnumerator(); try { while (e.MoveNext()) { V v = (V)(T)e.Current; embedded_statement } } finally { ... // Dispose e } }
変数 e は、式 x、埋め込みステートメント、またはプログラムのその他のソース コードからは、参照もアクセスもできません。変数 v は、埋め込みステートメントでは読み取り専用です。T (要素型) から V (foreach ステートメントの local-variable-type) への明示的な変換 (6.2 を参照) がない場合は、エラーが発生し、以降の手順は行われません。x が null 値を持つ場合、実行時に System.NullReferenceException がスローされます。
実装では、動作に上記の展開との整合性がある限り、パフォーマンス上の理由などから、異なる方法で foreach ステートメントを実装してもかまいません。
while ループ内への v の配置は、embedded-statement 内の匿名関数でどのようにキャプチャされるかに大きく影響します。
次に例を示します。int[] values = { 7, 9, 13 }; Action f = null; foreach (var value in values) { if (f == null) f = () => Console.WriteLine("First value: " + value); } f();
v が while ループの外側で宣言された場合、すべてのイテレーションで共有され、for ループの後の値が最終値 13 になります。これが f の呼び出しによって出力される値です。代わりに、各イテレーションには独自の変数 v があるため、最初のイテレーションで f によってキャプチャされた v は引き続き値 7 を保持し、この値が出力されます (メモ: 旧バージョンの C# では while ループの外側で v を宣言しました)。
finally ブロックの本体は、次の手順に従って構築されます。
E から System.IDisposable インターフェイスへの暗黙的な変換がある場合、次の処理が実行されます。
- E が null 非許容型である場合、finally 句は次の意味と同じになるように拡張されます。
finally { ((System.IDisposable)e).Dispose(); }
- それ以外の場合、finally 句は次の意味と同じになるように展開されます。
finally { if (e != null) ((System.IDisposable)e).Dispose(); }
ただし、E が値型または値型にインスタンス化される型パラメーターである場合は、e を System.IDisposable にキャストしてもボックス化は発生しません。 - それ以外の場合、E がシール型であれば、finally 句は空のブロックに展開されます。 ```csharp finally { }
- それ以外の場合、finally 句は次のように展開されます。
finally { System.IDisposable d = e as System.IDisposable; if (d != null) d.Dispose(); }
ローカル変数 d は、ユーザー コードから参照もアクセスもできません。特に、finally ブロックをスコープに含んでいる他の変数と衝突することはありません。
foreach が配列の要素を走査する順序は、次のように定義されます。1 次元配列の場合、要素はインデックス 0 から始まってインデックス Length – 1 で終わるインデックスの昇順に走査されます。多次元配列の場合、要素は、最初に右端の次元のインデックスが増加し、次にその左側の次元のインデックスが増加し、さらにその左側の次元のインデックスが増加する、という順序で走査されます。
2 次元配列のそれぞれの値を要素順にプリント アウトするコード例を次に示します。using System; class Test { static void Main() { double[,] values = { {1.2, 2.3, 3.4, 4.5}, {5.6, 6.7, 7.8, 8.9} }; foreach (double elementValue in values) Console.Write("{0} ", elementValue); Console.WriteLine(); } }
生成される出力は次のとおりです。
1.2 2.3 3.4 4.5 5.6 6.7 7.8 8.9
次に例を示します。int[] numbers = { 1, 3, 5, 7, 9 }; foreach (var n in numbers) Console.WriteLine(n);
n の型は、numbers の要素型である int と推論されます。
C# 言語仕様 Version 5.0より引用
おそらく最新は、https://github.com/dotnet/csharplang/blob/master/spec/statements.md#the-foreach-statement
色々書いてありますね。