[英]How to remove elements from a generic list while iterating over it?
我正在寻找一种更好的模式来处理每个需要处理的元素列表,然后根据结果从列表中删除。
您不能在foreach (var element in X)
.Remove(element)
使用.Remove(element)
(因为它会导致Collection was modified; enumeration operation may not execute.
异常)...您也不能使用for (int i = 0; i < elements.Count(); i++)
和.RemoveAt(i)
因为它破坏了你当前在集合中相对于i
。
有没有一种优雅的方法来做到这一点?
使用 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));
foreach (var item in list.ToList()) {
list.Remove(item);
}
如果您将“.ToList()”添加到您的列表(或 LINQ 查询的结果),您可以直接从“列表”中删除“项目”,而不会出现可怕的“集合已修改;枚举操作可能无法执行” 。 错误。 编译器复制“列表”,以便您可以安全地对数组进行删除。
虽然这种模式不是非常有效,但它具有自然的感觉,并且几乎可以在任何情况下灵活使用。 例如,当您想将每个“项目”保存到数据库并仅在数据库保存成功时将其从列表中删除时。
一个简单直接的解决方案:
使用在您的集合上向后运行的标准 for 循环和RemoveAt(i)
来删除元素。
当您想在迭代集合时从集合中删除元素时,首先要想到的是反向迭代。
幸运的是,有一个比编写 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);
}
}
在通用列表上使用 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);
}
选择你想要的元素,而不是试图删除你不想要的元素。 这比删除元素要容易得多(通常也更有效)。
var newSequence = (from el in list
where el.Something || el.AnotherThing < 0
select el);
我想将此作为评论发布,以回应下面迈克尔·狄龙 (Michael Dillon) 留下的评论,但它太长了,无论如何在我的回答中可能有用:
就个人而言,我永远不会一个一个地删除项目,如果您确实需要删除,则调用RemoveAll
,它接受一个谓词并且只重新排列内部数组一次,而Remove
为您删除的每个元素执行Array.Copy
操作。 RemoveAll
效率要高得多。
当你向后迭代一个列表时,你已经有了要删除的元素的索引,所以调用RemoveAt
会更有效率,因为Remove
首先遍历列表以找到RemoveAt
的索引您尝试删除的元素,但您已经知道该索引。
总而言之,我看不出有任何理由在 for 循环中调用Remove
。 理想情况下,如果可能的话,使用上面的代码根据需要从列表中流式传输元素,这样根本不需要创建第二个数据结构。
使用 .ToList() 将复制您的列表,如以下问题中所述: ToList()--它是否创建新列表?
通过使用 ToList(),您可以从原始列表中删除,因为您实际上是在迭代副本。
foreach (var item in listTracked.ToList()) {
if (DetermineIfRequiresRemoval(item)) {
listTracked.Remove(item)
}
}
如果确定要删除哪些项目的函数没有副作用并且不会改变项目(它是一个纯函数),一个简单有效的(线性时间)解决方案是:
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>
典型实现,这将具有线性运行时间。
由于任何删除都是在您可以使用的条件下进行的
list.RemoveAll(item => item.Value == someValue);
List<T> TheList = new List<T>();
TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
您不能使用 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循环中删除物品。)
在迭代列表时在列表上使用Remove
或RemoveAt
故意变得困难,因为这样做几乎总是错误的。 你也许可以用一些聪明的技巧让它工作,但它会非常慢。 每次调用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);
我会从过滤掉您不想保留的元素的 LINQ 查询中重新分配列表。
list = list.Where(item => ...).ToList();
除非列表非常大,否则在执行此操作时应该没有显着的性能问题。
在迭代列表时从列表中删除项目的最佳方法是使用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;
});
假设谓词是元素的布尔属性,如果它为真,则应删除该元素:
int i = 0;
while (i < list.Count())
{
if (list[i].predicate == true)
{
list.RemoveAt(i);
continue;
}
i++;
}
我希望“模式”是这样的:
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);
}
这将使代码与程序员大脑中进行的过程保持一致。
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 循环向后循环遍历列表。 如果集合的大小发生变化,则向前执行此操作可能会出现问题,但向后执行此操作始终是安全的。
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();
有一个这里没有提到的选项。
如果您不介意在项目中的某处添加一些代码,您可以添加和扩展 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() 的简单性,而没有内存开销。
复制您正在迭代的列表。 然后从副本中取出并插入原件。 向后退会令人困惑,并且在并行循环时效果不佳。
var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();
Parallel.ForEach(iterableIds, id =>
{
ids.Remove(id);
});
在 C# 中,一种简单的方法是标记要删除的列表,然后创建一个新列表来迭代......
foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}
甚至更简单地使用 linq ....
list.RemoveAll(p=>p.Delete);
但值得考虑的是,在您忙于删除的同时其他任务或线程是否可以访问同一个列表,并且可能使用 ConcurrentList 代替。
我会这样做
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
我发现自己处于类似的情况,我必须删除给定List<T>
中的每个第n个元素。
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 个)被保留,则有必要复制 RF 项,并将列表缩小一项 NR 倍。 所有线性时间。
我的方法是我首先创建一个索引列表,它应该被删除。 然后我遍历索引并从初始列表中删除项目。 这看起来像这样:
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);
}
}
只是想添加我的 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
}
使用属性跟踪要删除的元素,并在处理后将它们全部删除。
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);
myList.RemoveAt(i--);
simples;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.