在 C# 中枚举时从 List<T> 中删除项目的智能方法

Posted

技术标签:

【中文标题】在 C# 中枚举时从 List<T> 中删除项目的智能方法【英文标题】:Intelligent way of removing items from a List<T> while enumerating in C# 【发布时间】:2011-11-03 19:56:47 【问题描述】:

我有一个经典案例,即尝试从集合中删除一个项目,同时在循环中枚举它:

List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach (int i in myIntCollection)

    if (i == 42)
        myIntCollection.Remove(96);    // The error is here.
    if (i == 25)
        myIntCollection.Remove(42);    // The error is here.

在发生更改后的迭代开始时,会抛出InvalidOperationException,因为枚举器不喜欢底层集合发生更改。

我需要在迭代时对集合进行更改。有很多模式可以用来避免这种情况,但似乎没有一个有很好的解决方案:

    不要在这个循环中删除,而是保留一个单独的“删除列表”,你在主循环之后处理。

    这通常是一个很好的解决方案,但在我的情况下,我需要将该项目作为“等待”立即消失,直到之后 真正删除项目的主循环改变了我的代码的逻辑流程。

    无需删除该项目,只需在该项目上设置一个标志并将其标记为非活动状态。然后添加模式 1 的功能来清理列表。

    满足我的所有需求,但这意味着必须更改大量代码才能在每次项目被检查时检查非活动标志访问。这对我来说太过繁琐了。

    以某种方式将模式 2 的思想融入到派生自 List&lt;T&gt; 的类中。此 Superlist 将处理非活动标志,事后删除对象,并且不会将标记为非活动的项目暴露给枚举消费者。基本上,它只是封装了模式 2(以及随后的模式 1)的所有想法。

    这样的类存在吗?有人有这方面的代码吗?还是有更好的办法?

    有人告诉我,访问 myIntCollection.ToArray() 而不是 myIntCollection 将解决问题并允许我在循环内删除。

    对我来说,这似乎是一个糟糕的设计模式,还是没问题?

详情:

该列表将包含许多项目,我将只删除其中的一些。

在循环内部,我将执行各种流程,添加、删除等,因此解决方案需要相当通用。

我需要删除的项目可能不是循环中的当前项目。例如,我可能在 30 项循环中的第 10 项上,需要删除第 6 项或第 26 项。因此,向后遍历数组将不再起作用。 ;o(

【问题讨论】:

对其他人可能有用的信息:Avoid Collection has been modified error(模式 1 的封装) 附注:列表有很多时间(通常为 O(N),其中 N 是列表的长度)移动值。如果确实需要有效的随机访问,则可以在 O(log N) 内实现删除,使用平衡二叉树来保存其根所在的子树中的节点数。它是一个隐含键(序列中的索引)的 BST。 请看答案:***.com/questions/7193294/… 【参考方案1】:

最好的解决方案通常是使用RemoveAll() 方法:

myList.RemoveAll(x => x.SomeProp == "SomeValue");

或者,如果您需要删除 某些 元素:

MyListType[] elems = new[]  elem1, elem2 ;
myList.RemoveAll(x => elems.Contains(x));

当然,这假设您的循环仅用于删除目的。如果您确实需要额外处理,那么最好的方法通常是使用forwhile 循环,因为这样您就不用使用枚举器了:

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

    // Do processing here, then...
    if (shouldRemoveCondition)
    
        myList.RemoveAt(i);
    

后退可确保您不会跳过任何元素。

对编辑的回应

如果您要删除看似任意的元素,最简单的方法可能是跟踪您要删除的元素,然后一次性将它们全部删除。像这样的:

List<int> toRemove = new List<int>();
foreach (var elem in myList)

    // Do some stuff

    // Check for removal
    if (needToRemoveAnElement)
    
        toRemove.Add(elem);
    


// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));

【讨论】:

关于您的回复:我需要在处理该项目期间立即删除这些项目,而不是在整个循环处理完毕之后。我正在使用的解决方案是将我想立即删除的任何项目设为 NULL,然后再将其删除。这不是一个理想的解决方案,因为我必须到处检查 NULL,但它确实有效。 想知道如果 'elem' 不是 int ,那么我们不能使用 RemoveAll ,因为它出现在编辑的响应代码上。【参考方案2】:

如果您必须同时枚举 List&lt;T&gt; 并从中删除,那么我建议只需使用 while 循环而不是 foreach

var index = 0;
while (index < myList.Count) 
  if (someCondition(myList[index])) 
    myList.RemoveAt(index);
   else 
    index++;
  

【讨论】:

我认为这应该是公认的答案。这使您可以考虑列表中的其余项目,而无需重新列出要删除的项目。【参考方案3】:

我知道这篇文章很旧,但我想我会分享对我有用的东西。

创建列表的副本以进行枚举,然后在 for each 循环中,您可以处理复制的值,并使用源列表删除/添加/任何内容。

private void ProcessAndRemove(IList<Item> list)

    foreach (var item in list.ToList())
    
        if (item.DeterminingFactor > 10)
        
            list.Remove(item);
        
    

【讨论】:

关于“.ToList()”的好主意!简单的头部拍打器,也适用于您不直接使用股票“删除...()”方法的情况。 虽然效率很低。 “非常低效”有多低效?您只是指创建新列表占用的额外空间吗?如果空间不是问题,那么这似乎是一个非常直观的直接答案。【参考方案4】:

当您需要遍历列表并可能在循环期间对其进行修改时,最好使用 for 循环:

for (int i = 0; i < myIntCollection.Count; i++)

    if (myIntCollection[i] == 42)
    
        myIntCollection.Remove(i);
        i--;
    

当然你必须小心,例如,每当删除一个项目时,我都会减少 i,否则我们将跳过条目(另一种方法是向后遍历列表)。

如果您有 Linq,那么您应该按照 dlev 的建议使用 RemoveAll

【讨论】:

仅当您删除当前元素时才有效。如果要删除任意元素,则需要检查其索引是在当前索引处/之前还是之后,以决定是否要--i 原始问题没有明确表示必须支持删除除当前元素之外的其他元素,@CompuChip。这个答案自澄清以来没有改变。 @Palec,我明白了,因此我的评论。【参考方案5】:

当您枚举列表时,将您想要保留的列表添加到新列表中。然后,将新列表分配给myIntCollection

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);

foreach(int i in myIntCollection)

    if (i want to delete this)
        ///
    else
        newCollection.Add(i);

myIntCollection = newCollection;

【讨论】:

【参考方案6】:

让我们添加你的代码:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

如果您想在 foreach 中更改列表,则必须输入 .ToList()

foreach(int i in myIntCollection.ToList())

    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);

【讨论】:

【参考方案7】:

对于那些可能有帮助的人,我编写了这个扩展方法来删除与谓词匹配的项目并返回已删除项目的列表。

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        
            T item = source[i];
            if (predicate(item))
            
                removed.Add(item);
                source.RemoveAt(i);
            
        
        return removed;
    

【讨论】:

【参考方案8】:

怎么样

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)

    myIntCollection.Remove(42); //The error is no longer here.

【讨论】:

在当前的C#中,对于任何可枚举,它都可以重写为foreach (int i in myIntCollection.ToArray()) myIntCollection.Remove(42); ,而List&lt;T&gt;甚至在.NET 2.0中也特别支持这种方法。【参考方案9】:

如果您对高性能感兴趣,可以使用两个列表。以下内容最小化垃圾收集,最大化内存局部性,并且从不实际从列表中删除一个项目,如果它不是最后一个项目,这是非常低效的。

private void RemoveItems()

    _newList.Clear();

    foreach (var item in _list)
    
        item.Process();
        if (!item.NeedsRemoving())
            _newList.Add(item);
    

    var swap = _list;
    _list = _newList;
    _newList = swap;

【讨论】:

【参考方案10】:

我只是想我会分享我对类似问题的解决方案,我需要在处理它们时从列表中删除它们。

所以基本上是“foreach”,它会在迭代后从列表中删除该项目。

我的测试:

var list = new List<TempLoopDto>();
list.Add(new TempLoopDto("Test1"));
list.Add(new TempLoopDto("Test2"));
list.Add(new TempLoopDto("Test3"));
list.Add(new TempLoopDto("Test4"));

list.PopForEach((item) =>

    Console.WriteLine($"Process item.Name");
);

Assert.That(list.Count, Is.EqualTo(0));

我使用扩展方法“PopForEach”解决了这个问题,该方法将执行一个操作,然后从列表中删除该项目。

public static class ListExtensions

    public static void PopForEach<T>(this List<T> list, Action<T> action)
    
        var index = 0;
        while (index < list.Count) 
            action(list[index]);
            list.RemoveAt(index);
        
    

希望这对任何人都有帮助。

【讨论】:

以上是关于在 C# 中枚举时从 List<T> 中删除项目的智能方法的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?

将xml反序列化为c#中的类时从XmlTextAttribute获取int值

C#在Dictionary中使用枚举作为键

C#移除List<T> 指定List<T> 数据项

在索引处的 List<T> 中添加值的 C# 方法

在 c# 中使用 SqlDataReader 填充 List<T>