10

首先,我知道由于显而易见的原因,这不可能开箱即用。

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

上面的片段是我见过的最可怕的事情之一。那么,你如何实现它呢?您可以使用 向后遍历列表for,但我也不喜欢这种解决方案。

我想知道的是:是否有从当前列表返回 IEnumerable 的方法/扩展,类似于浮动副本?LINQ 有许多扩展方法可以做到这一点,但你总是需要对它做一些事情,比如过滤(where, take...)。

我很期待这样的事情:

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

其中 .Shadow() 是:

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

例子

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

解决方案

这就是我解决它的方法,它就像一个魅力:

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

它不是那么快(显然),但它是安全的并且正是我打算做的。

4

6 回答 6

27

由于列表的实现方式,从列表中一一删除多个元素是 C# 反模式。

当然,可以使用 for 循环(而不是 foreach)来完成。或者可以通过制作列表的副本来完成。但这就是为什么不应该这样做。在 100000 个随机整数的列表中,这在我的机器上需要 2500 毫秒:

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

这需要 1250 毫秒:

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

而这两个分别需要 5 和 2 毫秒:

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

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

这是因为当您从列表中删除一个元素时,您实际上是从一个数组中删除,这是 O(N) 时间,因为您需要将删除的元素之后的每个元素向左移动一个位置。平均而言,这将是 N/2 个元素。

Remove(element) 也需要在删除之前找到元素。所以 Remove(element) 实际上总是需要 N 步 -elementindex找到元素的N - elementindex步骤,删除它的步骤 - 总共 N 步。

RemoveAt(index) 不需要找到元素,但它仍然需要移动底层数组,所以平均来说,一个 RemoveAt 是 N/2 步。

无论哪种方式,最终结果都是 O(N^2) 复杂度,因为您要删除多达 N 个元素。

相反,您应该使用 Linq,它将在 O(N) 时间内修改整个列表,或者滚动您自己的列表,但您不应该在循环中使用 Remove(或 RemoveAt)。

于 2013-06-21T11:19:19.627 回答
8

为什么不这样做:

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

创建原始副本并用于迭代,然后从现有副本中删除。

如果你真的需要你的扩展方法,你也许可以为用户创建一些更易读的东西,例如:

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

这与.ToList().

来电:

foreach(string item in myListOfStrings.Shadow())

于 2013-06-21T10:59:50.087 回答
3

您不要为此使用 LINQ 扩展方法 - 您可以显式创建一个新列表,如下所示:

foreach(string item in new List<string>(myListOfStrings)) {
    myListOfStrings.Remove(item);
}
于 2013-06-21T11:01:07.780 回答
3

您必须在迭代时创建原始列表的副本,如下所示:

        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);
        }
于 2013-06-21T11:03:48.657 回答
3

您的示例从字符串中删除所有项目,因此它相当于:

myListOfStrings.Clear();

它也相当于:

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

但是我认为您正在寻找的是一种删除谓词为真的项目的方法 - 这就是这样RemoveAll()做的。

所以你可以写,例如:

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

或使用任何其他谓词。

但是,这会更改原始列表;如果您只想要删除某些项目的列表副本,则可以使用普通的 Linq:

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

var filteredList = myListOfStrings.Where(x => x != "TEST").ToList(); 
于 2013-06-21T11:10:47.393 回答
-1

Picking up on the answer of svinja I do believe the most efficient way of solving this problem is by doing:

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.

于 2017-11-07T10:55:20.787 回答