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

Posted

技术标签:

【中文标题】迭代时如何从通用列表中删除元素?【英文标题】:How to remove elements from a generic list while iterating over it? 【发布时间】:2010-12-07 15:01:01 【问题描述】:

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

您不能在foreach (var element in X) 中使用.Remove(element)(因为它会导致Collection was modified; enumeration operation may not execute. 异常)...您也不能使用for (int i = 0; i < elements.Count(); i++).RemoveAt(i),因为它会破坏您在相对于i 的集合。

有没有优雅的方法来做到这一点?

【问题讨论】:

【参考方案1】:

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

for (int i = safePendingList.Count - 1; i >= 0; i--)

    // some code
    // safePendingList.RemoveAt(i);

例子:

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

或者,您可以使用带有谓词的RemoveAll method 进行测试:

safePendingList.RemoveAll(item => item.Value == someValue);

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

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

【讨论】:

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

如果您将“.ToList()” 添加到您的列表(或 LINQ 查询的结果),您可以直接从“列表”中删除“项目”,而无需使用可怕的“集合已修改;枚举操作可能无法执行。"错误。编译器会复制“list”,以便您可以安全地对数组进行删除。

虽然这种模式不是超级高效,但它给人一种自然的感觉,并且对于几乎任何情况都足够灵活。例如,当您想将每个“项目”保存到数据库并仅在数据库保存成功时将其从列表中删除。

【讨论】:

如果效率不重要,这是最好的解决方案。 这也更快、更易读:list.RemoveAll(i => true); @Greg Little ,我是否理解正确 - 当您添加 ToList() 编译器会通过复制的集合但从原始集合中删除? 如果您的列表包含重复的项目,而您只想删除列表中稍后出现的项目,那么这不会删除错误的项目吗? @Pyrejkee 是的,那是因为如果您遍历原始列表,当您删除一个项目时,它会给出一个错误,指出集合已被修改,因此 foreach 循环会崩溃。使用列表的副本,项目不会从副本中删除,而只会从原始列表中删除,从而允许循环完成并修改原始列表【参考方案3】:

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

使用标准 for 循环在您的集合上运行向后,并使用RemoveAt(i) 删除元素。

【讨论】:

请注意,如果您的列表包含许多项目,则一次删除一个项目不会有效。它有可能是 O(n^2)。想象一个包含 20 亿个项目的列表,其中前十亿个项目最终都被删除了。每次删除都会强制复制所有后续项目,因此您最终会复制十亿个项目十亿次。这不是因为反向迭代,而是因为一次删除。 RemoveAll 确保每个项目最多复制一次,因此它是线性的。一次删除可能会慢十亿倍。 O(n) 与 O(n^2)。 @BruceDawson,这个结论是基于对 RemoveAll 内部结构的观察吗? @AaA - 我的观察不是基于查看 RemoveAll 内部结构,而是基于我对 C# 列表如何实现的理解。它只是一组项目,删除单个项目必然需要随后将所有项目向下移动一个位置。因此,此处建议的算法将具有 O(n^2) 性能。这是不必要的。有一些简单的算法可以在 O(n) 时间内完成这项任务,速度可能快数百万倍。我相信 remove_if 是正确的功能。它复制每个项目一次,而不是最多 n 次。【参考方案4】:

当您想在对集合进行迭代时从集合中删除元素时,首先应该想到反向迭代。

幸运的是,有一个比编写 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);
    

【讨论】:

这对我来说非常有效。简单、优雅,只需对我的代码进行最少的更改。 这只是天才吗?我同意@StephenMacDougall,我不需要使用那些 C++'y for 循环,只需按预期使用 LINQ。 我看不出比简单的 foreach (int myInt in test.ToList()) if (myInt % 2 == 0) test.Remove(myInt); 有任何优势 你仍然需要为 Reverse 分配一个副本,它引入了 Huh? moment - 为什么会有 Reverse。 @jedesah 是的,Reverse&lt;T&gt;() 创建了一个向后遍历列表的迭代器,但它为其分配了与列表本身大小相同的额外缓冲区 (referencesource.microsoft.com/#System.Core/System/Linq/…) . Reverse&lt;T&gt; 不只是以相反的顺序遍历原始列表(不分配额外的内存)。因此ToList()Reverse() 具有相同的内存消耗(都创建副本),但ToList() 不对数据做任何事情。使用Reverse&lt;int&gt;(),我想知道为什么列表颠倒了,出于什么原因。 @jahav 我明白你的意思。 Reverse&lt;T&gt;() 的实现创建了一个新缓冲区,这非常令人失望,我不太确定我是否理解为什么这是必要的。在我看来,根据Enumerable的底层结构,至少在某些情况下应该可以在不分配线性内存的情况下实现反向迭代。【参考方案5】:

在通用列表上使用 ToArray() 允许您在通用列表上执行 Remove(item):

        List<String> strings = new List<string>()  "a", "b", "c", "d" ;
        foreach (string s in strings.ToArray())
        
            if (s == "b")
                strings.Remove(s);
        

【讨论】:

这没有错,但我必须指出,这绕过了创建要删除的项目的第二个“存储”列表的需要,代价是将整个列表复制到数组中。第二个精心挑选的元素列表可能会有更少的项目。【参考方案6】:

选择您需要想要的元素,而不是尝试删除您不需要想要的元素。这比删除元素要容易得多(通常也更有效)。

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);

我想将此作为评论发布,以回应迈克尔狄龙在下面留下的评论,但它太长了,而且可能对我的回答有用:

就个人而言,我永远不会一个接一个地删除项目,如果您确实需要删除,然后调用 RemoveAll 它接受一个谓词并且只重新排列内部数组一次,而 Remove 执行 Array.Copy 操作对于您删除的每个元素。 RemoveAll 效率要高得多。

当您向后迭代列表时,您已经有了要删除的元素的索引,因此调用RemoveAt 会更有效,因为Remove 首先会遍历list 以查找您要删除的元素的索引,但您已经知道该索引。

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

【讨论】:

要么这样做,要么将指向不需要元素的指针添加到第二个列表中,然后在循环结束后,迭代删除列表并使用它来删除元素。 多么酷的解决方案,实际上它更有效。【参考方案7】:

使用 .ToList() 将复制您的列表,如以下问题所述: ToList()-- Does it Create a New List?

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

foreach (var item in listTracked.ToList())     

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

     

【讨论】:

但是从性能的角度来看,您正在复制您的列表,这可能需要一些时间。很好且简单的方法,但性能不太好【参考方案8】:

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

list.RemoveAll(condition);

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

var toRemove = new HashSet<T>();
foreach(var item in items)

     ...
     if(condition)
          toRemove.Add(item);

items.RemoveAll(toRemove.Contains);

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

最后,如果您的列表只是IList&lt;T&gt; 而不是List&lt;T&gt;,我建议我对How can I do this special foreach iterator? 的回答。与许多其他答案的二次运行时间相比,给定 IList&lt;T&gt; 的典型实现,这将具有线性运行时间。

【讨论】:

【参考方案9】:

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

list.RemoveAll(item => item.Value == someValue);

【讨论】:

如果处理不会使物品发生变异并且没有副作用,那是最好的解决方案。【参考方案10】:
List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));

【讨论】:

【参考方案11】:

您不能使用 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--);
    

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

【讨论】:

【参考方案12】:

在迭代该列表时在列表上使用RemoveRemoveAt 故意变得困难,因为这几乎总是错误的做法。你也许可以用一些巧妙的技巧让它工作,但它会非常慢。每次调用Remove 时,它都必须扫描整个列表以找到要删除的元素。每次调用 RemoveAt 时,它都必须将后续元素向左移动 1 个位置。因此,任何使用 RemoveRemoveAt 的解决方案都需要二次时间,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);

【讨论】:

【参考方案13】:

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

list = list.Where(item => ...).ToList();

除非列表非常大,否则这样做应该不会出现重大的性能问题。

【讨论】:

【参考方案14】:

在迭代列表时从列表中删除项目的最佳方法是使用 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;
);

【讨论】:

【参考方案15】:

假设 predicate 是一个元素的布尔属性,如果它为真,那么该元素应该被移除:

        int i = 0;
        while (i < list.Count())
        
            if (list[i].predicate == true)
            
                list.RemoveAt(i);
                continue;
            
            i++;
        

【讨论】:

我对此表示赞同,因为有时按顺序(而不是倒序)浏览列表可能更有效。也许您可以在找到第一个不删除的项目时停下来,因为列表是有序的。 (想象一下本例中 i++ 所在的“中断”。【参考方案16】:

希望“模式”是这样的:

foreach( thing in thingpile )

    if( /* condition#1 */ )
    
        foreach.markfordeleting( thing );
    
    elseif( /* condition#2 */ )
    
        foreach.markforkeeping( thing );
    
 
foreachcompleted

    // then the programmer's choices would be:

    // delete everything that was marked for deleting
    foreach.deletenow(thingpile); 

    // ...or... keep only things that were marked for keeping
    foreach.keepnow(thingpile);

    // ...or even... make a new list of the unmarked items
    others = foreach.unmarked(thingpile);   

这将使代码与程序员大脑中的过程保持一致。

【讨论】:

很简单。只需创建一个布尔标志数组(使用三态类型,例如Nullable&lt;bool&gt;,如果您想允许未标记),然后在 foreach 之后使用它来删除/保留项目。【参考方案17】:
foreach(var item in list.ToList())



if(item.Delete) list.Remove(item);


只需从第一个列表创建一个全新的列表。我说“简单”而不是“正确”,因为创建一个全新的列表可能比以前的方法具有更高的性能(我没有打扰任何基准测试。)我通常更喜欢这种模式,它也可以用于克服Linq-To-Entities 限制。

for(i = list.Count()-1;i>=0;i--)



item=list[i];

if (item.Delete) list.Remove(item);


这种方式通过一个普通的旧 For 循环向后循环列表。如果集合的大小发生变化,向前执行此操作可能会出现问题,但向后执行应该始终是安全的。

【讨论】:

【参考方案18】:

在 C# 中,一种简单的方法是标记要删除的那些,然后创建一个新列表以进行迭代...

foreach(var item in list.ToList())if(item.Delete) list.Remove(item);  

甚至更简单的使用 linq....

list.RemoveAll(p=>p.Delete);

但值得考虑的是其他任务或线程是否可以在您忙于删除的同时访问同一个列表,并且可能使用 ConcurrentList 代替。

【讨论】:

【参考方案19】:

这里有一个选项没有提到。

如果您不介意在项目中的某处添加一些代码,您可以添加和扩展 List 以返回一个类的实例,该实例会反向遍历列表。

你会这样使用它:

foreach (var elem in list.AsReverse())

    //Do stuff with elem
    //list.Remove(elem); //Delete it if you want

这是扩展的样子:

public static class ReverseListExtension

    public static ReverseList<T> AsReverse<T>(this List<T> list) => new ReverseList<T>(list);

    public class ReverseList<T> : IEnumerable
    
        List<T> list;
        public ReverseList(List<T> list) this.list = list; 

        public IEnumerator GetEnumerator()
        
            for (int i = list.Count - 1; i >= 0; i--)
                yield return list[i];
            yield break;
        
    

这基本上是 list.Reverse() 没有分配。

就像有些人提到的那样,您仍然会遇到逐个删除元素的缺点,如果您的列表很长,这里的一些选项会更好。但我认为有些人会想要 list.Reverse() 的简单性,而不需要内存开销。

【讨论】:

【参考方案20】:

For 循环是一个糟糕的结构。

使用while

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

while (numbers.Count > 0)

    numbers.RemoveAt(0);

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

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

for (; numbers.Count > 0;)

    numbers.RemoveAt(0);

或者,这个:

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

用法:

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

但是,LINQ 已经有 RemoveAll() 来执行此操作

var numbers = new List<int>(Enumerable.Range(1, 10));
numbers.RemoveAll((n) => n > 5);

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

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

【讨论】:

【参考方案21】:

复制您正在迭代的列表。然后从副本中删除并插入原件。后退会让人困惑,并且在并行循环时效果不佳。

var ids = new List<int>  1, 2, 3, 4 ;
var iterableIds = ids.ToList();

Parallel.ForEach(iterableIds, id =>

    ids.Remove(id);
);

【讨论】:

【参考方案22】:

只是想加我的 2 美分,以防万一这对任何人都有帮助,我遇到了类似的问题,但需要在迭代时从数组列表中删除多个元素。在我遇到错误并意识到在某些情况下索引大于数组列表的大小之前,最高支持的答案大部分都是为我做的,因为正在删除多个元素但循环的索引没有保留跟踪。我通过一个简单的检查解决了这个问题:

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--)
    if(i>= place_holder.Count)
        i = place_holder.Count-1; 
    

// some method that removes multiple elements here

【讨论】:

谢谢,对我的案子也很有帮助!【参考方案23】:

我愿意这样做

using System.IO;
using System;
using System.Collections.Generic;

class Author
    
        public string Firstname;
        public string Lastname;
        public int no;
    

class Program

    private static bool isEven(int i) 
     
        return ((i % 2) == 0); 
     

    static void Main()
        
        var authorsList = new List<Author>()
        
            new Author Firstname = "Bob", Lastname = "Smith", no = 2 ,
            new Author Firstname = "Fred", Lastname = "Jones", no = 3 ,
            new Author Firstname = "Brian", Lastname = "Brains", no = 4 ,
            new Author Firstname = "Billy", Lastname = "TheKid", no = 1 
        ;

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        
    

输出

Fred Jones
Billy TheKid

【讨论】:

【参考方案24】:

我发现自己处于类似情况,我必须删除给定 List&lt;T&gt; 中的每个第 nth 元素。

for (int i = 0, j = 0, n = 3; i < list.Count; i++)

    if ((j + 1) % n == 0) //Check current iteration is at the nth interval
    
        list.RemoveAt(i);
        j++; //This extra addition is necessary. Without it j will wrap
             //down to zero, which will throw off our index.
    
    j++; //This will always advance the j counter

【讨论】:

【参考方案25】:

从列表中删除一项的成本与要删除的项之后的项数成正比。在前半部分项目符合删除条件的情况下,任何基于单独删除项目的方法最终都必须执行大约 N*N/4 项复制操作,如果列表很大,这可能会变得非常昂贵.

一种更快的方法是扫描列表以找到要删除的第一个项目(如果有),然后从该点开始将每个应保留的项目复制到它所属的位置。完成此操作后,如果应保留 R 个项目,则列表中的前 R 个项目将是那些 R 个项目,所有需要删除的项目将在最后。如果这些项目以相反的顺序被删除,系统最终将不必复制它们中的任何一个,所以如果列表有 N 个项目,其中 R 个项目,包括所有前 F,被保留, 有必要复制 R-F 项,并将列表缩小 N-R 次。所有线性时间。

【讨论】:

【参考方案26】:

我的方法是首先创建一个索引列表,然后将其删除。然后我遍历索引并从初始列表中删除项目。这看起来像这样:

var messageList = ...;
// Restrict your list to certain criteria
var customMessageList = messageList.FindAll(m => m.UserId == someId);

if (customMessageList != null && customMessageList.Count > 0)

    // Create list with positions in origin list
    List<int> positionList = new List<int>();
    foreach (var message in customMessageList)
    
        var position = messageList.FindIndex(m => m.MessageId == message.MessageId);
        if (position != -1)
            positionList.Add(position);
    
    // To be able to remove the items in the origin list, we do it backwards
    // so that the order of indices stays the same
    positionList = positionList.OrderByDescending(p => p).ToList();
    foreach (var position in positionList)
    
        messageList.RemoveAt(position);
    

【讨论】:

【参考方案27】:

使用属性跟踪要删除的元素,并在处理后将它们全部删除。

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)

    if (cond) 
    
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    

// RESET
_Group.RemoveAll(r => r.REMOVE_ME);

【讨论】:

_Group.RemoveAll(condition(r)) 有什么问题? "cond" 有时可能是假的【参考方案28】:
myList.RemoveAt(i--);

simples;

【讨论】:

simples; 在这里做什么? 它是一个代表.. 它每次运行时都会对我的答案投反对票 S.W.滚!一定会喜欢你的评论。

以上是关于迭代时如何从通用列表中删除元素?的主要内容,如果未能解决你的问题,请参考以下文章

java.util.ConcurrentModificationException 从数组列表中删除元素时,即使使用迭代器

在列表迭代期间从 java.util.List 中删除元素时引发 ConcurrentModificationException? [复制]

c#在遍历列表时删除元素-向后迭代或使用i--或使用linq同时迭代和删除?

从 SwiftUI 的列表中删除列表元素

从 SwiftUI 的列表中删除列表元素

从包含特定字符的列表中删除元素[重复]