C#で列挙しながらリスト<T>からアイテムを削除するインテリジェントな方法


87

ループで列挙しながらコレクションからアイテムを削除しようとする典型的なケースがあります。

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. このループ内で削除しないでください。代わりに、メインループの後に処理する別の「リストの削除」を保持してください。

    これは通常は良い解決策ですが、私の場合、アイテムを実際に削除するためのメインループがコードのロジックフローを変更するまで、アイテムを「待機中」として即座に削除する必要があります。

  2. アイテムを削除する代わりに、アイテムにフラグを設定し、非アクティブとしてマークするだけです。次に、パターン1の機能を追加して、リストをクリーンアップします。

    これ私のすべてのニーズに対応しますが、アイテムにアクセスするたびに非アクティブフラグをチェックするために、多くのコードを変更する必要があることを意味します。これは私の好みにはあまりにも多くの管理です。

  3. どういうわけか、から派生したクラスにパターン2のアイデアを組み込みList<T>ます。このスーパーリストは、非アクティブフラグ、事後のオブジェクトの削除を処理し、非アクティブとしてマークされたアイテムを列挙型コンシューマーに公開しません。基本的には、パターン2(およびその後のパターン1)のすべてのアイデアをカプセル化するだけです。

    このようなクラスは存在しますか?誰かがこれのためのコードを持っていますか?それとももっと良い方法はありますか?

  4. myIntCollection.ToArray()代わりにアクセスmyIntCollectionすると問題が解決し、ループ内で削除できるようになると言われています。

    これは私には悪いデザインパターンのように思えますか、それとも問題ないのでしょうか?

詳細:

  • リストには多くのアイテムが含まれ、そのうちのいくつかのみを削除します。

  • ループ内では、追加、削除など、あらゆる種類のプロセスを実行するため、ソリューションはかなり一般的である必要があります。

  • 削除する必要のあるアイテムは、ループ内の現在のアイテムではない可能性があります。たとえば、30アイテムループのアイテム10にいて、アイテム6またはアイテム26を削除する必要がある場合があります。このため、配列を逆方向に歩くことはできなくなります。; o(


他の誰かに役立つ可能性のある情報:Avoid Collectionが変更されましたエラー(パターン1のカプセル化)
George Duckett 2011

補足:リストは値を移動するために多くの時間を節約します(通常はO(N)、Nはリストの長さです)。効率的なランダムアクセスが本当に必要な場合は、ルートがサブツリー内のノードの数を保持する平衡二分木を使用して、O(log N)で削除を実行できます。これは、キー(シーケンス内のインデックス)が暗示されているBSTです。
パレック2016年

:答えを参照してくださいstackoverflow.com/questions/7193294/...
Dabbas

回答:


196

最善の解決策は通常、次のRemoveAll()方法を使用することです。

myList.RemoveAll(x => x.SomeProp == "SomeValue");

または、特定の要素を削除する必要がある場合:

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

もちろん、これはループが削除のみを目的としていることを前提としています。追加の処理必要な場合は、通常、fororwhileループを使用するのが最善の方法です。これは、列挙子を使用していないためです。

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));

Regarding your response: I need to remove the items instantly during the processing of that item not after the whole loop has been processed. The solution I'm using is to NULL any items that I want to delete instantly and delete them afterwards. This is not an ideal solution as I have to check for NULL all over the place, but it DOES work.
John Stock

Wondering If 'elem' is not an int , then we can't use RemoveAll that way it is present on the edited response code.
User M

22

If you must both enumerate a List<T> and remove from it then I suggest simply using a while loop instead of a foreach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

This should be the accepted answer in my opinion. This allows you to consider the rest of the items in your list, without re-iterating a shortlist of items to be removed.
Slvrfn

13

I know this post is old, but I thought I'd share what worked for me.

Create a copy of the list for enumerating, and then in the for each loop, you can process on the copied values, and remove/add/whatever with the source list.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

Great idea on the ".ToList()"! Simple head-slapper, and also works in cases where you're not using the stock "Remove...()" meths directly.
galaxis

1
Very inefficient though.
nawfal

8

When you need to iterate through a list and might modify it during the loop then you are better off using a for loop:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

Of course you must be careful, for example I decrement i whenever an item is removed as otherwise we will skip entries (an alternative is to go backwards though the list).

If you have Linq then you should just use RemoveAll as dlev has suggested.


Only works when you are removing the current element though. If you are removing an arbitrary element, you will need to check whether its index was at/before or after the current index to decide whether to --i.
CompuChip

The original question did not make it clear that deletion of other elements than the current one must be supported, @CompuChip. This answer has not changed since it was clarified.
Palec

@Palec, I understand, hence my comment.
CompuChip

5

As you enumerate the list, add the one you want to KEEP to a new list. Afterward, assign the new list to the 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;

3

Let's add you code:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

If you want to change the list while you're in a foreach, you must type .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

1

For those it may help, I wrote this Extension method to remove items matching the predicate and return the list of removed items.

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    {
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        {
            T item = source[i];
            if (predicate(item))
            {
                removed.Add(item);
                source.RemoveAt(i);
            }
        }
        return removed;
    }

0

How about

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

In current C#, it can be rewritten as foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); } for any enumerable, and List<T> specifically supports this method even in .NET 2.0.
Palec

0

If you're interested in high performance, you can use two lists. The following minimises garbage collection, maximises memory locality and never actually removes an item from a list, which is very inefficient if it's not the last item.

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;
}

0

Just figured I'll share my solution to a similar problem where i needed to remove items from a list while processing them.

So basically "foreach" that will remove the item from the list after it has been iterated.

My test:

var list = new List<TempLoopDto>();
list.Add(new TempLoopDto("Test1"));
list.Add(new TempLoopDto("Test2"));
list.Add(new TempLoopDto("Test3"));
list.Add(new TempLoopDto("Test4"));

list.PopForEach((item) =>
{
    Console.WriteLine($"Process {item.Name}");
});

Assert.That(list.Count, Is.EqualTo(0));

I solved this with a extension method "PopForEach" that will perform a action and then remove the item from the list.

public static class ListExtensions
{
    public static void PopForEach<T>(this List<T> list, Action<T> action)
    {
        var index = 0;
        while (index < list.Count) {
            action(list[index]);
            list.RemoveAt(index);
        }
    }
}

Hope this can be helpful to any one.

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.