我正在寻找一种更好的模式,用于处理每个元素的列表,然后根据结果从列表中删除。
你不能在foreach (var element in X)中使用.Remove(element)(因为它会导致Collection被修改;枚举操作可能无法执行。例外)……你也不能使用for (int I = 0;i < elements.Count();i++)和. removeat (i),因为它破坏了你在集合中相对于i的当前位置。
有没有一种优雅的方式来做到这一点?
我正在寻找一种更好的模式,用于处理每个元素的列表,然后根据结果从列表中删除。
你不能在foreach (var element in X)中使用.Remove(element)(因为它会导致Collection被修改;枚举操作可能无法执行。例外)……你也不能使用for (int I = 0;i < elements.Count();i++)和. removeat (i),因为它破坏了你在集合中相对于i的当前位置。
有没有一种优雅的方式来做到这一点?
当前回答
如果决定删除哪些项的函数没有副作用,也没有改变项(这是一个纯函数),一个简单有效的(线性时间)解决方案是:
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,但是当你删除一个项目时,你可以向前迭代并管理你的循环索引变量,如下所示:
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循环期间删除项。)
在泛型列表上使用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);
}
Using Remove or RemoveAt on a list while iterating over that list has intentionally been made difficult, because it is almost always the wrong thing to do. You might be able to get it working with some clever trick, but it would be extremely slow. Every time you call Remove it has to scan through the entire list to find the element you want to remove. Every time you call RemoveAt it has to move subsequent elements 1 position to the left. As such, any solution using Remove or RemoveAt, would require quadratic time, 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);
我会这样做
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循环是一个不好的构造。
使用时
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();