イテレータの簡単な要約が必要な気がします。
反復子 (C# の IEnumerator および IEnumerable) は、基になる表現を公開することなく、順序付けられた方法で構造体の要素にアクセスするために使用されます。その結果、次のような非常に一般的な関数を使用できるようになります。
void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V>
{
foreach (V value in collection)
actor(value);
}
//Or the more verbose way
void Iterator<T, V>(T collection, Action<V> actor) where T : IEnumerable<V>
{
using (var iterator = collection.GetEnumerator())
{
while (iterator.MoveNext())
actor(iterator.Current);
}
}
//Or if you need to support non-generic collections (ArrayList, Queue, BitArray, etc)
void Iterator<T, V> (T collection, Action<V> actor) where T : IEnumerable
{
foreach (object value in collection)
actor((V)value);
}
C# 仕様に見られるように、トレードオフがあります。
5.3.3.16 Foreach ステートメント
foreach ( expr の型識別子) 埋め込みステートメント
これは単に、値が読み取り専用であることを意味します。なぜ読み取り専用なのですか? それは簡単です。は非常に高レベルのステートメントであるためforeach
、繰り返し処理しているコンテナーについて何も想定することはできません。二分木を繰り返し処理していて、foreach ステートメント内で値をランダムに割り当てることにした場合はどうなるでしょうか。foreach
読み取り専用アクセスを強制しなかった場合、バイナリ ツリーはツリーに退化します。データ構造全体が混乱します。
しかし、これはあなたの最初の質問ではありませんでした。最初の要素にアクセスする前にコレクションを変更していたため、エラーがスローされました。なんで?このために、ILSpyを使用して List クラスを掘り下げました。List クラスのスニペットを次に示します。
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
private int _version;
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
{
private List<T> list;
private int version;
private int index;
internal Enumerator(List<T> list)
{
this.list = list;
this.version = list._version;
this.index = 0;
}
/* All the implemented functions of IEnumerator<T> and IEnumerator will throw
a ThrowInvalidOperationException if (this.version != this.list._version) */
}
}
列挙子は、親リストの「バージョン」と親リストへの参照で初期化されます。 すべての反復操作は、初期バージョンが参照リストの現在のバージョンと同等であることを確認するためにチェックします。それらが同期していない場合、反復子は無効になります。なぜBCLはこれを行うのですか? 実装者が列挙子のインデックスが 0 (新しい列挙子を表す) であるかどうかを確認しなかったのはなぜですか?0 である場合は、単純にバージョンを再同期します。わからない。チームが IEnumerable を実装するすべてのクラス間での適合を望んでおり、それをシンプルに保ちたいと考えていたという仮説しか立てられません。したがって、List の列挙子 (および他のほとんどの列挙子) は、要素が範囲内にある限り、要素を区別しません。
これが問題の根本原因です。この機能が絶対に必要な場合は、独自のイテレータを実装する必要があり、最終的に独自の List を実装する必要がある場合があります。私の意見では、BCL の流れに逆らうにはあまりにも多くの作業が必要です。
以下は、BCL チームがおそらく従ったイテレータを設計する際のGoFからの引用です。
トラバース中に集計を変更するのは危険です。集約に対して要素が追加または削除されると、要素に 2 回アクセスしたり、完全に欠落したりする可能性があります。簡単な解決策は、集約をコピーしてコピーをトラバースすることですが、一般的にはコストが高すぎます
BCL チームは、時空間の複雑さと人的資源の点で費用がかかりすぎると判断した可能性が高いです。そして、この哲学は C# 全体に見られます。おそらく、foreach 内の変数の変更を許可するにはコストがかかりすぎ、リストの列挙子がリスト内のどこにあるかを判別するにはコストがかかりすぎ、ユーザーを抱きしめるにはコストがかかりすぎます。イテレータのパワーと制約を理解できるように、十分に説明できたことを願っています。
参考:
リストの「バージョン」を変更し、現在のすべての列挙子を無効にするものは何ですか?
- インデクサーによる要素の変更
Add
AddRange
Clear
Insert
InsertRange
RemoveAll
RemoveAt
RemoveRange
Reverse
Sort