ループで列挙しながらアイテムをコレクションから削除しようとする古典的なケースがあります:
List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);
foreach (int i in myIntCollection)
{
if (i == 42)
myIntCollection.Remove(96); // The error is here.
if (i == 25)
myIntCollection.Remove(42); // The error is here.
}
変更が行われた後の反復の開始時に、InvalidOperationException
がスローされます。これは、列挙子が基になるコレクションが変更されたときに気に入らないためです。
繰り返しながらコレクションを変更する必要があります。多くのこれを回避するために使用できるパターンがありますが、どれも良い解決策がないようです:
このループ内で削除しないでください。代わりに、メインループの後に処理する別の「リストの削除」を保持してください。
これは通常良い解決策ですが、私の場合、メインループがアイテムを本当に削除してコードのロジックフローが変更されるまで、アイテムを即座に「待機」状態にしておく必要があります。
アイテムを削除する代わりに、アイテムにフラグを設定し、非アクティブとしてマークするだけです。次に、パターン1の機能を追加してリストをクリーンアップします。
このwouldは私のすべてのニーズに対応しますが、lotのコードは、アイテムにアクセスするたびに非アクティブフラグをチェックするために変更する必要があります。これは私の好みにはあまりにも多すぎる管理です。
何らかの方法でパターン2のアイデアをList<T>
から派生したクラスに組み込みます。このスーパーリストは、非アクティブフラグ、事後のオブジェクトの削除を処理し、非アクティブとしてマークされたアイテムを列挙コンシューマーに公開しません。基本的には、パターン2(およびその後のパターン1)のすべてのアイデアをカプセル化します。
このようなクラスは存在しますか?誰でもこのためのコードを持っていますか?または、より良い方法がありますか?
myIntCollection
の代わりにmyIntCollection.ToArray()
にアクセスすると問題が解決し、ループ内で削除できると言われました。
これは私にとって悪いデザインパターンのように思えますか、それともいいのでしょうか。
詳細:
リストには多くの項目が含まれますが、そのうちの一部のみを削除します。
ループ内では、あらゆる種類のプロセス、追加、削除などを行うため、ソリューションはかなり汎用的である必要があります。
削除する必要があるアイテムは、ループ内の現在のアイテムではありません。たとえば、30項目ループの項目10にいて、項目6または項目26を削除する必要がある場合があります。これにより、配列を逆方向にたどることができなくなります。 ; o(
通常、最善の解決策は RemoveAll()
メソッドを使用することです。
myList.RemoveAll(x => x.SomeProp == "SomeValue");
または、certain要素を削除する必要がある場合:
MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));
これは、もちろん、ループが削除目的のみを目的としていることを前提としています。追加の処理が必要な場合doを使用する場合は、通常、for
またはwhile
ループを使用するのが最善の方法です。列挙子:
for (int i = myList.Count - 1; i >= 0; i--)
{
// Do processing here, then...
if (shouldRemoveCondition)
{
myList.RemoveAt(i);
}
}
後方に移動すると、要素をスキップしないことが保証されます。
編集への応答:
一見任意の要素を削除する場合、最も簡単な方法は、削除する要素を追跡し、その後一度にすべて削除することです。このようなもの:
List<int> toRemove = new List<int>();
foreach (var elem in myList)
{
// Do some stuff
// Check for removal
if (needToRemoveAnElement)
{
toRemove.Add(elem);
}
}
// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));
List<T>
を列挙して削除する必要がある場合は、while
の代わりにforeach
ループを使用することをお勧めします
var index = 0;
while (index < myList.Count) {
if (someCondition(myList[index])) {
myList.RemoveAt(index);
} else {
index++;
}
}
私はこの投稿が古いことを知っていますが、私は私のために働いたものを共有すると思いました。
列挙用のリストのコピーを作成してから、for eachループで、コピーされた値を処理し、ソースリストで削除/追加/何でもできます。
private void ProcessAndRemove(IList<Item> list)
{
foreach (var item in list.ToList())
{
if (item.DeterminingFactor > 10)
{
list.Remove(item);
}
}
}
リストを反復処理する必要があり、ループ中にリストを変更する場合は、forループを使用することをお勧めします。
for (int i = 0; i < myIntCollection.Count; i++)
{
if (myIntCollection[i] == 42)
{
myIntCollection.Remove(i);
i--;
}
}
もちろん、注意する必要があります。たとえば、項目が削除されるたびにi
をデクリメントします。
Linqをお持ちの場合は、dlevが示唆しているようにRemoveAll
を使用する必要があります。
リストを列挙するときに、保持したいものを新しいリストに追加します。その後、myIntCollection
に新しいリストを割り当てます
List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);
foreach(int i in myIntCollection)
{
if (i want to delete this)
///
else
newCollection.Add(i);
}
myIntCollection = newCollection;
コードを追加しましょう:
List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);
Foreachの実行中にリストを変更する場合は、.ToList()
と入力する必要があります
foreach(int i in myIntCollection.ToList())
{
if (i == 42)
myIntCollection.Remove(96);
if (i == 25)
myIntCollection.Remove(42);
}
どう?
int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
myIntCollection.Remove(42); //The error is no longer here.
}
高いパフォーマンスに関心がある場合は、2つのリストを使用できます。次は、ガベージコレクションを最小化し、メモリの局所性を最大化し、リストからアイテムを実際に削除することはありません。これは、最後のアイテムでない場合は非常に非効率的です。
private void RemoveItems()
{
_newList.Clear();
foreach (var item in _list)
{
item.Process();
if (!item.NeedsRemoving())
_newList.Add(item);
}
var swap = _list;
_list = _newList;
_newList = swap;
}