从 List中删除项目的智能方式在 C# 中枚举时

发布于 2024-12-01 13:08:53 字数 1439 浏览 1 评论 0原文

我有一个经典案例,尝试从集合中删除项目,同时在循环中枚举它:

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 的类中。该超级列表将处理非活动标志、事后删除对象,并且也不会向枚举使用者公开标记为非活动的项目。基本上,它只是封装了模式 2(以及随后的模式 1)的所有思想。

    有这样的类吗?有人有这方面的代码吗?还是有更好的方法?

  4. 有人告诉我,访问 myIntCollection.ToArray() 而不是 myIntCollection 将解决问题并允许我在循环内删除。

    这对我来说似乎是一个糟糕的设计模式,或者也许它很好?

详细信息:

  • 列表将包含许多项目,我将仅删除其中一些项目。

  • 在循环内部,我将执行各种过程,添加、删除等,因此解决方案需要相当通用。

  • 我需要删除的项目可能不是循环中的当前项目。例如,我可能位于 30 项循环的第 10 项上,需要删除第 6 项或第 26 项。因此,向后遍历数组将不再有效。 ;o(

I have the classic case of trying to remove an item from a collection while enumerating it in a loop:

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

At the beginning of the iteration after a change takes place, an InvalidOperationException is thrown, because enumerators don’t like when the underlying collection changes.

I need to make changes to the collection while iterating. There are many patterns that can be used to avoid this, but none of them seems to have a good solution:

  1. Do not delete inside this loop, instead keep a separate “Delete List”, that you process after the main loop.

    This is normally a good solution, but in my case, I need the item to be gone instantly as “waiting” till after
    the main loop to really delete the item changes the logic flow of my code.

  2. Instead of deleting the item, simply set a flag on the item and mark it as inactive. Then add the functionality of pattern 1 to clean up the list.

    This would work for all of my needs, but it means that a lot of code will have to change in order to check the inactive flag every time an item is accessed. This is far too much administration for my liking.

  3. Somehow incorporate the ideas of pattern 2 in a class that derives from List<T>. This Superlist will handle the inactive flag, the deletion of objects after the fact and also will not expose items marked as inactive to enumeration consumers. Basically, it just encapsulates all the ideas of pattern 2 (and subsequently pattern 1).

    Does a class like this exist? Does anyone have code for this? Or is there a better way?

  4. I’ve been told that accessing myIntCollection.ToArray() instead of myIntCollection will solve the problem and allow me to delete inside the loop.

    This seems like a bad design pattern to me, or maybe it’s fine?

Details:

  • The list will contain many items and I will be removing only some of them.

  • Inside the loop, I will be doing all sorts of processes, adding, removing etc., so the solution needs to be fairly generic.

  • The item that I need to delete may not be the current item in the loop. For example, I may be on item 10 of a 30 item loop and need to remove item 6 or item 26. Walking backwards through the array will no longer work because of this. ;o(

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

乖乖 2024-12-08 13:08:53

最好的解决方案通常是使用 < code>RemoveAll() 方法:

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

或者,如果您需要删除某些元素:

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

当然,这假设您的循环仅用于删除目的。如果您确实需要额外的处理,那么最好的方法通常是使用forwhile循环,因为这样您就不会使用枚举器:

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

The best solution is usually to use the RemoveAll() method:

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

Or, if you need certain elements removed:

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

This assume that your loop is solely intended for removal purposes, of course. If you do need to additional processing, then the best method is usually to use a for or while loop, since then you're not using an enumerator:

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

Going backwards ensures that you don't skip any elements.

Response to Edit:

If you're going to have seemingly arbitrary elements removed, the easiest method might be to just keep track of the elements you want to remove, and then remove them all at once after. Something like this:

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));
二智少女猫性小仙女 2024-12-08 13:08:53

如果您必须枚举 List 并从中删除,那么我建议简单地使用 while 循环而不是 foreach

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

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++;
  }
}
妳是的陽光 2024-12-08 13:08:53

我知道这篇文章很旧,但我想我应该分享对我有用的内容。

创建列表的副本以进行枚举,然后在 foreach 循环中,您可以处理复制的值,并使用源列表删除/添加/执行任何操作。

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

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);
        }
    }
}
瑾兮 2024-12-08 13:08:53

当您需要迭代列表并可能在循环期间修改它时,最好使用 for 循环:

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

当然,您必须小心,例如,每当删除一个项目时,我都会递减 i :否则我们将跳过条目(另一种方法是向后浏览列表)。

如果您有 Linq,那么您应该按照 dlev 的建议使用 RemoveAll

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.

寻找一个思念的角度 2024-12-08 13:08:53

当您枚举列表时,将要保留的列表添加到新列表中。然后,将新列表分配给 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;

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;
假情假意假温柔 2024-12-08 13:08:53

让我们添加代码:

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

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);
}
笔芯 2024-12-08 13:08:53

对于那些可能有帮助的人,我编写了这个扩展方法来删除与谓词匹配的项目并返回已删除项目的列表。

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

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;
    }
尽揽少女心 2024-12-08 13:08:53

怎么样

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

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.
}
扬花落满肩 2024-12-08 13:08:53

如果您对高性能感兴趣,可以使用两个列表。下面的代码最大限度地减少了垃圾收集,最大化了内存局部性,并且从不实际从列表中删除项目,如果它不是最后一个项目,则效率非常低。

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

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;
}
柏拉图鍀咏恒 2024-12-08 13:08:53

只是想我将分享我对类似问题的解决方案,在处理它们时我需要从列表中删除项目。

所以基本上“foreach”会在迭代后从列表中删除该项目。

我的测试:

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

我使用扩展方法“PopForEach”解决了这个问题,该方法将执行一个操作,然后从列表中删除该项目。

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

希望这对任何人都有帮助。

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(
quot;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.

卸妝后依然美 2024-12-08 13:08:53

目前您正在使用一个列表。如果你可以用字典来代替,那就容易多了。我假设您实际上使用的是一个类,而不仅仅是一个整数列表。如果您有某种形式的唯一密钥,这将起作用。在字典中,object 可以是您拥有的任何类,int 可以是任何唯一的键。

    Dictionary<int, object> myIntCollection = new Dictionary<int, object>();
        myIntCollection.Add(42, "");
        myIntCollection.Add(12, "");
        myIntCollection.Add(96, "");
        myIntCollection.Add(25, "");


        foreach (int i in myIntCollection.Keys)
        {
            //Check to make sure the key wasn't already removed
            if (myIntCollection.ContainsKey(i))
            {
                if (i == 42) //You can test against the key
                    myIntCollection.Remove(96);
                if (myIntCollection[i] == 25) //or you can test against the value
                    myIntCollection.Remove(42);    
            }
        }

或者您可以使用

    Dictionary<myUniqueClass, bool> myCollection; //Bool is just an empty place holder

好处是您可以对底层字典执行任何您想要的操作,并且键枚举器不关心,但它也不会随着添加或删除的条目而更新。

Currently you are using a list. If you could use a dictionary instead, it would be much easier. I'm making some assumptions that you are really using a class instead of just a list of ints. This would work if you had some form of unique key. In the dictionary, object can be any class you have and int would be any unique key.

    Dictionary<int, object> myIntCollection = new Dictionary<int, object>();
        myIntCollection.Add(42, "");
        myIntCollection.Add(12, "");
        myIntCollection.Add(96, "");
        myIntCollection.Add(25, "");


        foreach (int i in myIntCollection.Keys)
        {
            //Check to make sure the key wasn't already removed
            if (myIntCollection.ContainsKey(i))
            {
                if (i == 42) //You can test against the key
                    myIntCollection.Remove(96);
                if (myIntCollection[i] == 25) //or you can test against the value
                    myIntCollection.Remove(42);    
            }
        }

Or you could use

    Dictionary<myUniqueClass, bool> myCollection; //Bool is just an empty place holder

The nice thing is you can do anything you want to the underlying dictionary and the key enumerator doesn't care, but it also doesn't update with added or removed entries.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文