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


450

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

您不能.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

有没有一种优雅的方法可以做到这一点?

Answers:


733

用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方法来测试:

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

19
对于那些来自Java的人,C#的List就像ArrayList一样,其中插入/删除为O(n),通过索引进行的检索为O(1)。这不是传统的链表。似乎有点不幸,C#使用单词“ List”来描述此数据结构,因为它使我想到了经典的链表。
Jarrod Smith

79
名称“列表”中没有任何内容表示“ LinkedList”。来自Java以外的其他语言的人们在将其作为链表时可能会感到困惑。
GolezTrol

5
我在这里通过vb.net搜索结束,以防万一有人想要RemoveAll的vb.net等效语法:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder

1
这也可以在Java中进行最小的修改:.size()代替.Count.remove(i)代替.removeAt(i)。聪明-谢谢!
秋季伦纳德

2
我对性能进行了一些测试,结果发现它所RemoveAll()花的时间是向后for循环的三倍。因此,我绝对会坚持使用循环,至少在重要的部分。
Crusha K. Rool

84

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

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


2
请注意,如果列表中包含许多项目,则一次删除一个项目效率高。它有可能是O(n ^ 2)。想象一个有20亿个项目的列表,恰好是前十亿个项目最终都被删除了。每次删除都会强制复制所有以后的项目,因此最终您将十亿个项目复制十亿次。这不是因为反向迭代,而是因为一次删除。RemoveAll确保每个项目最多复制一次,因此是线性的。一次删除可能会慢十亿倍。O(n)对O(n ^ 2)。
布鲁斯·道森

71

当您想在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);
    }
}

7
这对我来说非常有效。简单,优雅,并且只需对我的代码进行最少的更改。
Stephen MacDougall

1
这只是图钉天才吗?我同意@StephenMacDougall,我不需要使用那些C ++'y进行循环,而只需按预期使用LINQ。
Piotr Kula 2014年

5
与简单的foreach相比,我看不出任何优势(test.ToList()中的int myInt){if(myInt%2 == 0){test.Remove(myInt); 您仍然必须为Reverse分配一个副本,它引入了吗?片刻-为什么会有Reverse。
jahav 2015年

11
@jedesah是的,Reverse<T>()创建一个迭代器,该迭代器向后遍历列表,但会为其分配与列表本身大小相同的额外缓冲区referencesource.microsoft.com/#System.Core/System/Linq/…)。Reverse<T>不只是以相反的顺序浏览原始列表(不分配额外的内存)。因此,两者ToList()Reverse()具有相同的内存消耗(均创建副本),但ToList()对数据不做任何事情。使用Reverse<int>(),我想知道为什么列表被反转,是什么原因。
jahav 2015年

1
@jahav我明白你的意思了。令人失望的是,该实现Reverse<T>()创建了一个新缓冲区,我不确定我是否理解为什么这样做是必要的。在我看来,根据的底层结构,Enumerable至少在某些情况下应该可以实现反向迭代而无需分配线性内存。
jedesah 2015年

68
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

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

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


6
如果效率不是至关重要的,那么这是最佳解决方案。
IvanP '16

2
这也更快,更易读:list.RemoveAll(i => true);
圣地亚哥·亚利桑那州,2016年

1
@Greg Little,我是否正确理解您-当您添加ToList()编译器会经历复制的集合但会从原始副本中删除吗?
Pyrejkee

如果您的列表中包含重复的项目,而您只想删除列表中稍后出现的项目,那么这不会删除错误的项目吗?
Tim MB

这也适用于对象列表吗?
汤姆·埃尔萨法迪

23

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

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

我想将其作为评论发布,以回应下面迈克尔·狄龙(Michael Dillon)留下的评论,但是它太长了,可能无论如何对我的回答有用:

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

而且,当您向后遍历列表时,您已经有了要删除的元素的索引,因此调用效率会更高RemoveAt,因为Remove首先遍历列表以查找您要元素的索引正在尝试删除,但您已经知道该索引。

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


1
或者,或者将指向不需要的元素的指针添加到第二个列表中,然后在循环结束之后,迭代删除列表并使用该列表删除元素。
Michael Dillon

22

在常规列表上使用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);
        }

2
这没有错,但我必须指出,这绕过了创建要删除的项目的第二个“存储”列表的需要,而无需将整个列表复制到数组中。手工挑选元素的第二个列表可能包含较少的项目。
艾哈迈德·玛吉

20

使用.ToList()将复制您的列表,如以下问题所述: ToList()-是否创建新列表?

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

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

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

     }

1
但是从性能的角度来看,您正在复制列表,这可能需要一些时间。简便易行的方法,但效果却不佳
Florian K

12

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

list.RemoveAll(condition);

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

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

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

最后,如果您的列表只是一个列表,IList<T>而不是列表,List<T>我建议我对如何执行此特殊的foreach迭代器进行回答IList<T>与许多其他答案的二次运行时相比,给定的典型实现将具有线性运行时。


11

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

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

如果处理过程不会使项目发生变异并且没有副作用,那将是最好的解决方案。
CodesInChaos 2014年


8

您不能使用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循环中删除项目。)


6

有意地在遍历该列表时使用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);

3

希望 “模式”是这样的:

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

这将使代码与程序员大脑中进行的处理保持一致。


1
很简单。只需创建一个布尔标志数组(使用三态类型,例如Nullable<bool>,如果要允许未标记),然后在foreach之后使用它即可删除/保留项目。
丹·贝查德

3

通过假定谓词是元素的布尔属性,则如果谓词为true,则应删除该元素:

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

我对此表示赞同,因为有时按顺序(而不是相反的顺序)遍历列表可能会更有效率。也许您会发现找不到要删除的第一项时就停止了,因为该列表是有序的。(想象一下在此示例中i ++所在的“中断”位置
。– FrankKrumnow

3

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

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

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


3

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

2
foreach(var item in list.ToList())

{

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

}

只需从第一个列表创建一个全新的列表。我说“容易”而不是“正确”,因为创建一个全新的列表可能会比以前的方法具有更高的性能(我没有为任何基准测试感到烦恼。)我通常更喜欢这种模式,它在克服方面也很有用Linq到实体的限制。

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

{

item=list[i];

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

}

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


1

复制您要迭代的列表。然后从副本中取出并插入原件。向后走是令人困惑的,并且在并行循环时不能很好地工作。

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

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});

1

我会这样

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

0

我发现自己处于类似情况,必须删除给定元素中的每n 元素List<T>

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
}

0

从列表中删除一项的成本与要删除的项之后的项数成正比。如果前半部分项目有资格删除,则任何基于单独删除项目的方法最终都必须执行大约N * N / 4个项目复制操作,如果列表很大,这将变得非常昂贵。

一种更快的方法是浏览列表以找到要删除的第一个项目(如果有的话),然后从该点开始,将应保留的每个项目复制到其所属的位置。完成此操作后,如果应保留R个项目,则列表中的前R个项目将是这些R个项目,所有需要删除的项目都将在末尾。如果以相反的顺序删除了这些项目,则系统不必再复制任何项目,因此,如果列表中有N个项目被保留,其中R个项目(包括所有前F个项目)都已保留,则必须复制RF项,并将列表缩小NR次。所有线性时间。


0

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

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

0

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

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

甚至更简单地使用linq。

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

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


0

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

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

0

我只是想在此基础上加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
}

-4
myList.RemoveAt(i--);

simples;

1
simples;在这里做什么?
丹尼

6
其委托..它downvotes我的答案每次运行时
SW

SW ROFL!一定喜欢您的评论。
埃里克·布朗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.