如何在迭代时从通用列表中删除元素?

Inv*_*ion 418 c# generics loops list key-value

我正在寻找一个更好的模式来处理每个需要处理的元素列表,然后根据结果从列表中删除.

你不能.Remove(element)在里面使用foreach (var element in X)(因为它导致Collection was modified; enumeration operation may not execute.异常)...你也不能使用for (int i = 0; i < elements.Count(); i++),.RemoveAt(i)因为它会扰乱你在集合中的当前位置i.

有一种优雅的方式来做到这一点?

Ahm*_*eed 682

使用for循环反向迭代列表:

for (int i = safePendingList.Count - 1; i >= 0; i--)
{
    // some code
    // safePendingList.RemoveAt(i);
}
Run Code Online (Sandbox Code Playgroud)

例:

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i] > 5)
        list.RemoveAt(i);
}
list.ForEach(i => Console.WriteLine(i));
Run Code Online (Sandbox Code Playgroud)

或者,您可以将RemoveAll方法与谓词一起使用来测试:

safePendingList.RemoveAll(item => item.Value == someValue);
Run Code Online (Sandbox Code Playgroud)

这是一个简化的例子来演示:

var list = new List<int>(Enumerable.Range(1, 10));
Console.WriteLine("Before:");
list.ForEach(i => Console.WriteLine(i));
list.RemoveAll(i => i > 5);
Console.WriteLine("After:");
list.ForEach(i => Console.WriteLine(i));
Run Code Online (Sandbox Code Playgroud)

  • "List"名称中没有任何内容表示"LinkedList".来自Java之外的其他语言的人可能会在链接列表时感到困惑. (68认同)
  • 对于那些来自Java的人来说,C#的List就像ArrayList一样,插入/删除是O(n),通过索引检索是O(1).这不是传统的链表.看起来有点不幸C#使用"List"这个词来描述这个数据结构,因为它让人联想到经典的链表. (17认同)
  • 我最后通过vb.net搜索到这里,以防万一有人想要RemoveAll的vb.net等效语法:`list.RemoveAll(Function(item)item.Value = somevalue)` (4认同)
  • 我对性能进行了一些测试,结果发现`RemoveAll()`所需的时间是向后`for`循环的三倍.所以我肯定坚持循环,至少在重要的部分. (2认同)
  • @nl-x 区别在于你使用它的时间。在您正在迭代的同一个集合上使用“.Remove()”,并使用“foreach”,就会出现此错误。使用 `RemoveAt(...)` 并反向使用 `for` 循环,可以让我们删除一个元素,同时跟踪索引以避免超出范围。当使用“RemoveAll()”时,它不会在循环中使用,因此不必担心修改集合本身,因为我们没有迭代它。 (2认同)

Jan*_*Jan 83

一个简单直接的解决方案:

使用标准for循环向后运行集合并RemoveAt(i)删除元素.

  • 请注意,如果您的列表包含许多项目,则一次删除一项“效率不高”。它有可能是 O(n^2)。想象一个包含 20 亿个项目的列表,而前 10 亿个项目最终都被删除了。每次删除都会强制复制所有后续项目,因此您最终会每次复制 10 亿个项目十亿次。这不是因为反向迭代,而是因为一次删除一个。RemoveAll 确保每个项目最多复制一次,因此它是线性的。一次删除一个可能会慢十亿倍。O(n) 与 O(n^2)。 (3认同)

jed*_*sah 66

当您想要在迭代时从Collection中删除元素时,首先应该想到反向迭代.

幸运的是,有一种比写一个for循环更优雅的解决方案,它涉及不必要的打字并且容易出错.

ICollection<int> test = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

foreach (int myInt in test.Reverse<int>())
{
    if (myInt % 2 == 0)
    {
        test.Remove(myInt);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @jedesah是的,`Reverse <T>()`创建一个向后遍历列表的迭代器,但是**为它分配了与列表本身大小相同的额外缓冲区**(http://referencesource.microsoft.com/ #System.Core/System/Linq/Enumerable.cs,792).`Reverse <T>`不会以相反的顺序遍历原始列表(不分配额外的内存).因此`ToList()`和`Reverse()`都有相同的内存消耗(都是创建副本),但`ToList()`对数据没有任何作用.使用`Reverse <int>()`,我想知道为什么列表会被反转,原因是什么. (11认同)
  • 这对我很有用.简单,优雅,并且需要对我的代码进行最小的更改. (6认同)
  • 我没有看到任何优于简单的foreach(test.ToList()中的int myInt){if(myInt%2 == 0){test.Remove(myInt); 你仍然需要为Reverse分配一个副本,它引入了Huh?那一刻 - 为什么会有逆转. (5认同)

Gre*_*tle 56

 foreach (var item in list.ToList()) {
     list.Remove(item);
 }
Run Code Online (Sandbox Code Playgroud)

如果向列表中添加".ToList()"(或LINQ查询的结果),则可以直接从"列表"中删除"item",而不会删除可疑的 " Collection被修改;枚举操作可能无法执行".错误.编译器会复制"list",以便您可以安全地对阵列执行删除操作.

虽然这种模式不是非常有效,但它具有自然的感觉,并且对于几乎任何情况都足够灵活.例如,当您想要将每个"项目"保存到数据库并仅在数据库保存成功时将其从列表中删除.

  • 如果效率并不重要,这是最佳解决方案. (5认同)
  • @Greg Little,我对你的理解是否正确 - 当你添加 ToList() 编译器会遍历复制的集合但从原始集合中删除? (4认同)
  • 这也更快,更易读:list.RemoveAll(i => true); (2认同)

小智 21

在通用列表上使用ToArray()允许您在通用列表上执行删除(项目):

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }
Run Code Online (Sandbox Code Playgroud)

  • 这没有错,但我必须指出,这绕过了创建第二个"存储"列表中所需项目的需要,但代价是将整个列表复制到数组.第二个精选元素列表可能会有更少的项目. (2认同)

Jul*_*anR 21

选择您的元素想要,而不是试图消除你的元素想要的.这比删除元素更容易(并且通常也更有效).

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);
Run Code Online (Sandbox Code Playgroud)

我想发布这篇评论作为评论,以回应迈克尔·狄龙在下面留下的评论,但它太长了,无论如何都可能在我的答案中有用:

就个人而言,我永远不会一个一个地删除项目,如果你确实需要删除,那么调用RemoveAll哪个获取谓词并且只重新排列内部数组一次,而RemoveArray.Copy你删除的每个元素执行操作.RemoveAll效率更高.

当你向后迭代列表时,你已经有了要删除的元素的索引,所以调用它会更有效率RemoveAt,因为Remove首先遍历列表以找到元素的索引你我试图删除,但你已经知道该索引.

总而言之,我认为没有任何理由要求Removefor-loop.理想情况下,如果可能的话,使用上面的代码根据需要从列表中流式传输元素,因此根本不需要创建第二个数据结构.


Stu*_*rtQ 18

使用.ToList()将创建列表的副本,如此问题中所述: ToList() - 它是否创建新列表?

通过使用ToList(),您可以从原始列表中删除,因为您实际上正在迭代副本.

foreach (var item in listTracked.ToList()) {    

        if (DetermineIfRequiresRemoval(item)) {
            listTracked.Remove(item)
        }

     }
Run Code Online (Sandbox Code Playgroud)

  • 但是从性能的角度来看,您正在复制您的列表,这可能需要一些时间。这样做的好方法和简单方法,但性能不太好 (2认同)

Cod*_*aos 12

如果确定要删除哪些项目的函数没有副作用且不改变项目(它是纯函数),则简单有效(线性时间)解决方案是:

list.RemoveAll(condition);
Run Code Online (Sandbox Code Playgroud)

如果有副作用,我会使用类似的东西:

var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);
Run Code Online (Sandbox Code Playgroud)

假设哈希是好的,这仍然是线性时间.但由于hashset,它的内存使用量增加了.

最后,如果你的列表只是一个IList<T>而不是List<T>我建议我的答案我怎么能做这个特殊的foreach迭代器?.IList<T>与许多其他答案的二次运行时相比,这将具有给定典型实现的线性运行时.


Ahm*_*mad 11

任何删除都是在您可以使用的条件下进行的

list.RemoveAll(item => item.Value == someValue);
Run Code Online (Sandbox Code Playgroud)


Mau*_*lho 9

List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
Run Code Online (Sandbox Code Playgroud)


Mel*_*per 9

For 循环对此来说是一个糟糕的构造。

使用while

var numbers = new List<int>(Enumerable.Range(1, 3));

while (numbers.Count > 0)
{
    numbers.RemoveAt(0);
}
Run Code Online (Sandbox Code Playgroud)

但是,如果你绝对必须使用for

var numbers = new List<int>(Enumerable.Range(1, 3));

for (; numbers.Count > 0;)
{
    numbers.RemoveAt(0);
}
Run Code Online (Sandbox Code Playgroud)

或这个:

public static class Extensions
{

    public static IList<T> Remove<T>(
        this IList<T> numbers,
        Func<T, bool> predicate)
    {
        numbers.ForEachBackwards(predicate, (n, index) => numbers.RemoveAt(index));
        return numbers;
    }

    public static void ForEachBackwards<T>(
        this IList<T> numbers,
        Func<T, bool> predicate,
        Action<T, int> action)
    {
        for (var i = numbers.Count - 1; i >= 0; i--)
        {
            if (predicate(numbers[i]))
            {
                action(numbers[i], i);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

var numbers = new List<int>(Enumerable.Range(1, 10)).Remove((n) => n > 5);
Run Code Online (Sandbox Code Playgroud)

然而,LINQ 已经必须RemoveAll()这样做

var numbers = new List<int>(Enumerable.Range(1, 3));

while (numbers.Count > 0)
{
    numbers.RemoveAt(0);
}
Run Code Online (Sandbox Code Playgroud)

最后,您可能最好使用 LINQWhere()来过滤和创建新列表,而不是改变现有列表。不变性通常是好的。

var numbers = new List<int>(Enumerable.Range(1, 10))
    .Where((n) => n <= 5)
    .ToList();
Run Code Online (Sandbox Code Playgroud)


yoy*_*oyo 7

您不能使用foreach,但是当您删除项目时,您可以向前迭代并管理循环索引变量,如下所示:

for (int i = 0; i < elements.Count; i++)
{
    if (<condition>)
    {
        // Decrement the loop counter to iterate this index again, since later elements will get moved down during the remove operation.
        elements.RemoveAt(i--);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,通常所有这些技术都依赖于迭代的集合的行为.此处显示的技术将与标准List(T)一起使用.(很有可能编写自己的集合类和迭代器,允许在foreach循环期间删除项目.)


Hüs*_*ğlı 6

在迭代列表时从列表中删除项目的最佳方法是使用RemoveAll(). 但人们主要担心的是他们必须在循环内做一些复杂的事情和/或有复杂的比较情况。

解决方案是仍然使用RemoveAll()但使用这种表示法:

var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(item => 
{
    // Do some complex operations here
    // Or even some operations on the items
    SomeFunction(item);
    // In the end return true if the item is to be removed. False otherwise
    return item > 5;
});
Run Code Online (Sandbox Code Playgroud)


Mar*_*age 5

我会从 LINQ 查询中重新分配列表,该查询会过滤掉您不想保留的元素。

list = list.Where(item => ...).ToList();
Run Code Online (Sandbox Code Playgroud)

除非列表非常大,否则执行此操作不会出现明显的性能问题。


bcm*_*inc 5

有意地遍历该列表时,在列表上使用RemoveRemoveAt在列表上变得困难,因为这样做几乎总是错误的。您可能可以通过一些巧妙的技巧使其工作,但是速度非常慢。每次调用时,Remove它都必须扫描整个列表以找到要删除的元素。每次调用时,RemoveAt它都必须将后续元素向左移动1个位置。因此,任何使用Remove或的解决方案RemoveAt都需要二次时间O(n²)

RemoveAll如果可以的话使用。否则,以下模式将在线性时间O(n)中就地过滤列表。

// Create a list to be filtered
IList<int> elements = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
// Filter the list
int kept = 0;
for (int i = 0; i < elements.Count; i++) {
    // Test whether this is an element that we want to keep.
    if (elements[i] % 3 > 0) {
        // Add it to the list of kept elements.
        elements[kept] = elements[i];
        kept++;
    }
}
// Unfortunately IList has no Resize method. So instead we
// remove the last element of the list until: elements.Count == kept.
while (kept < elements.Count) elements.RemoveAt(elements.Count-1);
Run Code Online (Sandbox Code Playgroud)