关于c#:如何在迭代时从泛型列表中删除元素?

How to remove elements from a generic list while iterating over it?

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

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的当前位置。

有优雅的方法吗?


用for循环反向迭代列表:

1
2
3
4
5
for (int i = safePendingList.Count - 1; i >= 0; i--)
{
    // some code
    // safePendingList.RemoveAt(i);
}

例子:

1
2
3
4
5
6
7
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方法来测试:

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

下面是一个简单的例子来演示:

1
2
3
4
5
6
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));


简单明了的解决方案:

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


当您希望在迭代过程中从集合中移除元素时,应该首先想到反向迭代。

幸运的是,有一个比编写for循环更优雅的解决方案,它涉及到不必要的输入,并且容易出错。

1
2
3
4
5
6
7
8
9
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);
    }
}


1
2
3
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

如果将".to list()"添加到列表(或Linq查询的结果),则可以直接从"list"中删除"item",而不必修改可怕的"collection";枚举操作可能不会执行。"error"。编译器生成"list"的副本,这样就可以安全地在数组上执行删除操作。

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


在通用列表上使用toarray()可以在通用列表上执行移除(项):

1
2
3
4
5
6
        List<String> strings = new List<string>() {"a","b","c","d" };
        foreach (string s in strings.ToArray())
        {
            if (s =="b")
                strings.Remove(s);
        }


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

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

我想把这篇文章作为对迈克尔·迪伦评论的回应发表在下面,但是这篇文章太长了,而且可能对我的回答很有用:

就我个人而言,我从不逐个删除项目,如果您确实需要删除,那么调用RemoveAll,它接受谓词,只重新排列一次内部数组,而Remove对您删除的每个元素执行Array.Copy操作。RemoveAll的效率大大提高。

当你反向迭代一个列表时,你已经有了你想要删除的元素的索引,所以调用RemoveAt会更有效,因为Remove首先遍历列表以找到你想要删除的元素的索引,但是你已经知道这个索引。

总之,我看不出有任何理由在for循环中调用Remove。理想情况下,如果可能的话,可以使用上面的代码根据需要从列表中流式地传递元素,这样就不需要再创建第二个数据结构。


使用.tolist()将复制列表,如本问题中所述:tolist()--它是否创建一个新列表?

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

1
2
3
4
5
6
7
foreach (var item in listTracked.ToList()) {    

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

     }


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

1
list.RemoveAll(condition);

如果有副作用,我会用如下方法:

1
2
3
4
5
6
7
8
var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

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

最后,如果您的列表只是一个IList,而不是一个List,我建议您回答如何使用这个特殊的foreach迭代器?。与许多其他答案的二次运行时间相比,考虑到IList的典型实现,这将具有线性运行时间。


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

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


1
2
3
List<T> TheList = new List<T>();

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

不能使用foreach,但可以在删除项时向前迭代并管理循环索引变量,如:

1
2
3
4
5
6
7
8
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--);
    }
}

注意,一般来说,所有这些技术都依赖于被迭代的集合的行为。这里显示的技术将与标准列表(t)一起工作。(很可能编写自己的集合类和迭代器,它允许在foreach循环期间删除项。)


在列表上使用RemoveRemoveAt,而在迭代该列表时故意使其变得困难,因为这样做几乎总是错误的。你也许可以用一些巧妙的技巧使它发挥作用,但速度会非常慢。每次调用Remove时,它都必须扫描整个列表以查找要删除的元素。每次调用RemoveAt时,都必须将后面的元素1位置移到左边。因此,任何使用RemoveRemoveAt的解决方案都需要二次时间o(n2)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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);

我将从一个筛选出您不希望保留的元素的Linq查询中重新分配列表。

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

除非列表非常大,否则在执行此操作时不应该存在显著的性能问题。


我希望"模式"是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
2
3
4
5
6
7
foreach(var item in list.ToList())

{

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

}

只需从第一个列表创建一个全新的列表。我说"简单"而不是"正确",因为创建一个全新的列表可能比以前的方法要高性能(我没有考虑任何基准测试)。我通常更喜欢这种模式,它还可以帮助克服LINQ对实体的限制。

1
2
3
4
5
6
7
8
9
for(i = list.Count()-1;i>=0;i--)

{

item=list[i];

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

}

这种方法使用一个普通的旧for循环向后循环列表。如果集合的大小改变了,那么向前做可能会有问题,但是向后做应该总是安全的。


在遍历列表时从列表中删除项的最佳方法是使用RemoveAll()。但是人们写的主要关注点是他们必须在循环中做一些复杂的事情和/或有复杂的比较案例。

解决方案是仍然使用RemoveAll(),但使用以下符号:

1
2
3
4
5
6
7
8
9
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;
});

假设谓词是元素的布尔属性,如果为真,则应删除该元素:

1
2
3
4
5
6
7
8
9
10
        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }


我发现自己也处于同样的情况,我必须删除给定List中的每个n元素。

1
2
3
4
5
6
7
8
9
10
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
}

从列表中删除项目的成本与要删除的项目后面的项目数成比例。如果前半个项目符合删除条件,则基于单独删除项目的任何方法最终都必须执行大约n*n/4个项目复制操作,如果列表很大,则可能会非常昂贵。

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


我的方法是首先创建一个索引列表,它应该被删除。然后,我循环遍历索引,并从初始列表中删除这些项。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
    }
}

复制正在迭代的列表。然后从副本中删除并与原始副本交互。向后走会让人困惑,并且在并行循环时不能很好地工作。

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

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

1
2
3
myList.RemoveAt(i--);

simples;