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

604
我正在寻找更好的模式来处理一组元素,每个元素都需要处理,然后根据结果从列表中移除。在foreach(var element in X)循环中不能使用.Remove(element),因为它会导致“集合已修改;可能不执行枚举操作。”异常......您也不能使用for(int i=0;i
有没有一种优雅的方式来实现这个?
28个回答

10

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

8

对于这种情况,使用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();

6

在遍历列表并删除其中的项目时,最好使用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;
});

这是进行清理的最佳方式,它会对对象进行最后的处理,然后停止跟踪它。 - HackSlash

6
在迭代一个列表时,使用 Remove 或者 RemoveAt 操作是故意被设计为困难的,因为这几乎总是错误的做法。你可能能够通过一些巧妙的技巧让它工作,但这会极其缓慢。每次调用 Remove 都需要扫描整个列表以找到要删除的元素。每次调用 RemoveAt 都需要将后续元素向左移动一个位置。因此,任何使用 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);

5
我会重新分配来自筛选掉您不想保留元素的LINQ查询列表。
list = list.Where(item => ...).ToList();

除非列表非常大,否则在执行此操作时不应出现任何重大性能问题。


4

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

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

我为此点赞,因为有时按顺序(而非相反顺序)移动列表可能更有效率。也许当找到第一个不需要删除的项时,您可以停止,因为该列表已经排序。 (在本示例中,想象一下“break”在i ++处的情况)。 - FrankKrumnow

4
在C#中,一种简单的方法是标记您希望删除的项,然后创建一个新列表进行迭代...
foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}  

甚至更简单的方法是使用linq...
list.RemoveAll(p=>p.Delete);

但是需要考虑的是,如果在您繁忙地进行删除操作时,其他任务或线程将同时访问同一列表,则可以考虑使用ConcurrentList。


2

这里有一个选项没有被提到。

如果您不介意在项目中添加一些代码,您可以添加一个扩展到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()的简单性,而没有内存开销。


2
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 循环方式可以倒序遍历列表。如果正向遍历,当集合大小发生变化时可能会出现问题,但倒序遍历应该总是安全的。


2

我希望“模式”是这样的:

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
很简单。只需创建一个布尔标志数组(如果要允许未标记,则使用3状态类型,例如Nullable<bool>),然后在foreach之后使用它来删除/保留项目。 - Dan Bechard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接