简体   繁体   English

在迭代时从列表中删除/添加项目

[英]Remove/Add items to/from a list while iterating it

First, I know this isn't possible out of the box because of obvious reasons. 首先,我知道由于显而易见的原因,这不可能开箱即用。

foreach(string item in myListOfStrings) {
    myListOfStrings.Remove(item);
}

The snipped above is one of the most horrible things I've ever seen. 上面的剪辑是我见过的最可怕的事情之一。 So, how do you achieve it then? 那么,你如何实现呢? You could iterate through the list backwards using for , but I don't like this solution either. 您可以使用for向后遍历列表,但我也不喜欢这个解决方案。

What I'm wondering is: Is there a method/extensions that returns an IEnumerable from the current list, something like a floating copy? 我想知道的是:是否有一个方法/扩展从当前列表返回IEnumerable,类似浮动副本? LINQ has numerous extension methods that do exactly this, but you always have to do something with it, such as filtering (where, take...). LINQ有很多扩展方法可以做到这一点,但你总是要对它做一些事情,比如过滤(where,take ......)。

I'm looking forward to something like this: 我期待着这样的事情:

foreach(string item in myListOfStrings.Shadow()) {
   myListOfStrings.Remove(item);
}

where as .Shadow() is: 其中.Shadow()是:

public static IEnumerable<T> Shadow<T>(this IEnumerable<T> source) {
    return new IEnumerable<T>(source);
    // or return source.Copy()
    // or return source.TakeAll();
}

Example

foreach(ResponseFlags flag in responseFlagsList.Shadow()) {
    switch(flag) {
        case ResponseFlags.Case1:
            ...
        case ResponseFlags.Case2:
            ...
    }
    ...
    this.InvokeSomeVoidEvent(flag)
    responseFlagsList.Remove(flag);
}

Solution

This is how I solved it, and it works like a charm: 这就是我解决它的方式,它就像一个魅力:

public static IEnumerable<T> Shadow<T>(this IEnumerable<T> source) where T: new() {
    foreach(T item in source)
        yield return item;
}

It's not that super fast (obviously), but it's safe and exactly what I intended to do. 它不是那么超快(显然),但它是安全的,正是我打算做的。

Removing multiple elements from a list 1 by 1 is a C# anti-pattern due to how lists are implemented. 由于列表的实现方式,逐个从列表中删除多个元素是C#反模式。

Of course, it can be done with a for loop (instead of foreach). 当然,它可以用for循环(而不是foreach)完成。 Or it can be done by making a copy of the list. 或者可以通过制作列表的副本来完成。 But here is why it should not be done . 但这就是为什么不应该这样做的原因 On a list of 100000 random integers, this takes 2500 ms on my machine: 在100000个随机整数列表中,我的机器需要2500毫秒:

       foreach (var x in listA.ToList())
            if (x % 2 == 0)
                listA.Remove(x);

and this takes 1250 ms: 这需要1250毫秒:

        for (int i = 0; i < listA.Count; i++)
            if (listA[i] % 2 == 0)
                listA.RemoveAt(i--);

while these two take 5 and 2 ms respectively: 而这两个分别需要5和2毫秒:

        listB = listB.Where(x => x % 2 != 0).ToList();

        listB.RemoveAll(x => x % 2 == 0);

This is because when you remove an element from a list, you are actually deleting from an array , and this is O(N) time, as you need to shift each element after the deleted element one position to the left. 这是因为当您从列表中删除元素时,实际上是从数组中删除,这是O(N)时间,因为您需要将删除元素之后的每个元素向左移动一个位置。 On average, this will be N/2 elements. 平均而言,这将是N / 2个元素。

Remove(element) also needs to find the element before removing it. 删除(元素)还需要在删除之前找到该元素。 So Remove(element) will actually always take N steps - elementindex steps to find the element, N - elementindex steps to remove it - in total, N steps. 所以Remove(element)实际上总是需要N步 - elementindex步骤来找到元素, N - elementindex步骤去除它 - 总共N步。

RemoveAt(index) doesn't have to find the element, but it still has to shift the underlying array, so on average, a RemoveAt is N/2 steps. RemoveAt(index)不必查找元素,但它仍然必须移动底层数组,因此平均而言,RemoveAt是N / 2步。

The end result is O(N^2) complexity either way, as you're removing up to N elements. 最终结果是O(N ^ 2)复杂度,因为您要删除多达N个元素。

Instead, you should use Linq, which will modify the entire list in O(N) time, or roll your own, but you should not use Remove (or RemoveAt) in a loop. 相反,你应该使用Linq,它将在O(N)时间内修改整个列表,或者自己滚动,但是你不应该在循环中使用Remove(或RemoveAt)。

Why not just do: 为什么不这样做:

foreach(string item in myListOfStrings.ToList()) 
{
    myListOfStrings.Remove(item);
}

To create a copy of the original and use for iterating, then remove from the existing. 要创建原始副本并用于迭代,请从现有中删除。

If you really need your extension method you could perhaps create something more readable to the user such as: 如果您确实需要扩展方法,则可以为用户创建更具可读性的内容,例如:

 public static IEnumerable<T> Shadow<T>(this IEnumerable<T> items)
 {
     if (items == null)
        throw new NullReferenceException("Items cannot be null");

     List<T> list = new List<T>();
     foreach (var item in items)
     {
         list.Add(item);
     }
     return list;
 }

Which is essentially the same as .ToList() . 这与.ToList()基本相同。

Calling: 呼叫:

foreach(string item in myListOfStrings.Shadow())

You do not LINQ extension methods for this - you can create a new list explicitly, like this: 你没有LINQ扩展方法 - 你可以显式创建一个新列表,如下所示:

foreach(string item in new List<string>(myListOfStrings)) {
    myListOfStrings.Remove(item);
}

You have to create a copy of the original list while iterating as below: 您必须在迭代时创建原始列表的副本,如下所示:

        var myListOfStrings = new List<string>();

        myListOfStrings.Add("1");
        myListOfStrings.Add("2");
        myListOfStrings.Add("3");
        myListOfStrings.Add("4");
        myListOfStrings.Add("5");

        foreach (string item in myListOfStrings.ToList())
        {
            myListOfStrings.Remove(item);
        }

Your example removes all items from the string, so it's equivalent to: 您的示例将从字符串中删除所有项目,因此它等效于:

myListOfStrings.Clear();

It is also equivalent to: 它也相当于:

myListOfStrings.RemoveAll(x => true); // Empties myListOfStrings

But what I think you're looking for is a way to remove items for which a predicate is true - which is what RemoveAll() does. 但我认为你正在寻找的方法是删除谓词为真的项目 - 这就是RemoveAll()作用。

So you could write, for example: 所以你可以写,例如:

myListOfStrings.RemoveAll(x => x == "TEST"); // Modifies myListOfStrings

Or use any other predicate. 或者使用任何其他谓词。

However, that changes the ORIGINAL list; 但是,这会更改ORIGINAL列表; If you just want a copy of the list with certain items removed, you can just use normal Linq: 如果您只想要删除某些项目的列表副本,您可以使用普通的Linq:

// Note != instead of == as used in Removeall(), 
// because the logic here is reversed.

var filteredList = myListOfStrings.Where(x => x != "TEST").ToList(); 

Picking up on the answer of svinja I do believe the most efficient way of solving this problem is by doing: 拿起svinja的答案,我相信解决这个问题的最有效方法是:

for (int i = 0; i < listA.Count;) {
    if (listA[i] % 2 == 0)
        listA.RemoveAt(i);
    else
        i++;
}

It improves on the answer by removing unnecessary sums and subtractions. 它通过删除不必要的总和和减法来改进答案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM