如何从泛型列表中删除元素,而迭代它?

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

你不能在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的当前位置。

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

509002 次浏览

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

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

除非列表非常大,否则这样做应该不会有明显的性能问题。

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

使用标准的for循环,在你的集合上运行向后RemoveAt(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));

在泛型列表上使用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首先遍历列表来找到你想要删除的元素的索引,但你已经知道那个索引。

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

当您希望在迭代Collection时从其中删除元素时,首先想到的应该是反向迭代。

幸运的是,有一个比编写for循环更优雅的解决方案,因为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);
}
}
List<T> TheList = new List<T>();


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

使用.ToList()将创建你的列表的副本,如这个问题所解释: # EYZ0 < / p >

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

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


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


}

我发现自己遇到了类似的情况,我必须删除给定List<T>中的每个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
}

我的“模式”是这样的:

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

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

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

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

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

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

如果决定删除哪些项的函数没有副作用,也没有改变项(这是一个纯函数),一个简单有效的(线性时间)解决方案是:

list.RemoveAll(condition);

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

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

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

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

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

如果你添加“.ToList()”到你的列表(或LINQ查询的结果),你可以直接从“列表”中删除“item”,而不会出现可怕的“修改集合;枚举操作可能无法执行.”错误。编译器会复制“list”,这样你就可以安全地删除数组了。

虽然这种模式不是超级高效,但它有一种自然的感觉,并且是足够灵活,几乎适用于任何情况。例如,当您想要将每个“项”保存到一个DB,并仅当DB保存成功时才将其从列表中删除。

你不能使用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循环期间删除项。)

myList.RemoveAt(i--);


simples;

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

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

在一个列表上使用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);

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

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

复制您正在迭代的列表。然后从副本中删除并与原件相互作用。倒退是令人困惑的,并且在并行循环时不能很好地工作。

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


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

在遍历列表时从列表中删除项目的最佳方法是使用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;
});
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循环向后遍历列表。如果集合的大小发生了变化,那么向前执行这个操作可能会有问题,但是向后执行应该总是安全的。

在c#中,一个简单的方法是标记你想要删除的,然后创建一个新的列表来迭代…

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

或者更简单的使用linq....

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

但是值得考虑的是,如果其他任务或线程在你忙着删除的同时访问同一个列表,那么可以使用ConcurrentList来代替。

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

using System.Linq;


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


bool cond = false;
foreach (MyProperty currObj in _Group)
{
// here it is supposed that you decide the "remove conditions"...
cond = true; // set true or false...
if (cond)
{
// SET - element can be deleted
currObj.REMOVE_ME = true;
}
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);

只是想添加我的2美分,以防这有助于任何人,我有一个类似的问题,但需要从一个数组列表中删除多个元素,而它正在被迭代。最高的upvotes的答案在很大程度上为我做了,直到我遇到错误,意识到在某些情况下,索引大于数组列表的大小,因为多个元素被删除,但循环的索引没有跟踪它。我用一个简单的检查解决了这个问题:

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.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

For循环是一个不好的构造。

使用# EYZ0

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()的简单性,没有内存开销。