Лучший способ перебрать список и удалить из него элементы?
Мне нужно выполнить итерацию по List<myObject>
и удалить элементы, которые отвечают определенному условию.
Я увидел этот ответ (qaru.site/info/20956/...):
Итерируйте свой список в обратном порядке с помощью цикла 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));
Но я понял, что for
менее эффективен, чем foreach
,
Итак, я подумал о том, чтобы использовать следующее:
foreach (var item in myList.ToList())
{
// if certain condition applies:
myList.Remove(item)
}
Один метод лучше другого?
EDIT:
Я не хочу использовать RemoveAll(...)
, так как в цикле есть большое количество кода, до условия.
Ответы
Ответ 1
Виллиально, вам нужно перебирать список, а цикл for
является наиболее эффективным:
for (int i = safePendingList.Count - 1; i >= 0; --i)
if (condition)
safePendingList.RemoveAt(i);
Если вы хотите удалить диапазон (не во всем списке), просто измените для цикла:
// No Enumarable.Range(1, 10) - put them into "for"
for (int i = Math.Min(11, safePendingList.Count - 1); i >= 1; --i)
if (condition)
safePendingList.RemoveAt(i);
Или, если вам нужно удалить элементы в прямом цикле:
for (int i = 0; i < safePendingList.Count;) // notice ++i abscence
if (condition)
safePendingList.RemoveAt(i);
else
i += 1; // ++i should be here
Наоборот, safePendingList.ToList()
создает копию начального safePendingList
, и это означает, что служебные данные памяти и процессора:
// safePendingList.ToList() - CPU and Memory overhead (copying)
foreach (var item in safePendingList.ToList()) {
if (condition)
myList.Remove(item); // Overhead: searching
}
Однако наиболее разумный план во многих случаях заключается в том, чтобы позволить .Net работать для вас:
safePendingList.RemoveAll(item => condition);
Ответ 2
чтобы удалить элементы с определенным условием, которое вы можете использовать
list.RemoveAll(item => item > 5);
вместо
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);
}
Ответ 3
Проблема заключается в том, что foreach
не может работать с коллекцией при ее изменении, это приведет к InvalidOperationException
.
Использование .ToList()
внутри foreach
позволяет нам избежать этого, но это копирует всю коллекцию, в результате которой получается циклический цикл в коллекции дважды (первый раз, когда он копирует все элементы и второй раз фильтрует его).
Цикл for
- лучший вариант, потому что мы можем пройти коллекцию один раз. Помимо этого вы можете фильтровать коллекцию с помощью метода LINQ
Where()
, что-то вроде строк:
var myFilteredCollection = myList.Where(i => i <= 5).ToList();
Если ваши условия сложны, вы можете переместить эту логику в отдельный метод:
private bool IsItemOk(myObject item)
{
... your complicated logic.
return true;
}
И используйте его в своем запросе LINQ:
var myFilteredCollection = myList.Where(i => IsItemOk(i)).ToList();
Другой вариант - это наоборот. Создайте новую коллекцию и добавьте элементы на основе условия. Чем вы можете использовать цикл foreach
:
var newList = new List<MyObject>(myList.Count);
foreach (var item in myList)
{
// if certain condition does not apply:
newList.Add(item);
}
P.S.
Но как уже упоминалось, .RemoveAll()
лучше, потому что вам не нужно "изобретать колесо"
Ответ 4
Ни один из методов не показывает существенных различий. for
и RemoveAll
выглядит немного быстрее (более вероятно, потому что, используя Remove
, вы дублируете свой список, и его нужно воссоздать... используя типы значений, это также означает удвоение необходимой памяти). Я сделал некоторые профилирования:
public static void MethodA()
{
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);
}
}
public static void MethodB()
{
var list = new List<int>(Enumerable.Range(1, 10));
foreach (var item in list.ToList())
{
if (item > 5)
list.Remove(item);
}
}
public static void MethodC()
{
var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(x => x > 5);
}
public static void Measure(string meas, Action act)
{
GC.Collect();
GC.WaitForPendingFinalizers();
var st = new Stopwatch();
st.Start();
for (var i = 0; i < 10000000; i++)
act();
st.Stop();
Console.WriteLine($"{meas}: {st.Elapsed}");
}
static void Main(string[] args)
{
Measure("A", MethodA);
Measure("B", MethodB);
Measure("C", MethodC);
}
Результат:
A: 00: 00: 04.1179163
B: 00: 00: 07.7417853
C: 00: 00: 04.3525255
Другие прогоны дают еще более похожие результаты (обратите внимание, что это для 10 миллионов итераций в списке из 10 пунктов [таким образом, выполняется 100 миллионов операций]... разница в ~ 3 секунды - это не то, что я назвал бы "значительным", но ваш пробег может меняться)
Итак, пойдите с тем, что, по вашему мнению, вы можете читать лучше (я лично использовал метод RemoveAll
)
Примечание: по мере роста вашего списка (это всего 10 элементов) MethodB
(foreach
тот, который дублирует список) будет медленнее, а различия будут выше, поскольку дублирование списка будет более дорогостоящим. Например, делая список из 1000 элементов (при условии, что на 500 вместо 5) и 100 000 итераций, MethodB
составляет 34 секунды, в то время как обе остальные работают на моей машине менее 2 секунд. Так что да, определенно не используйте MethodB
: -)
Это не означает, что foreach
менее эффективен, чем for
, это означает, что для использования foreach
для удаления элементов из списка вам необходимо дублировать указанный список, и это дорого.
Этот другой метод (с использованием foreach
) должен быть примерно таким же быстрым, как методы for
или RemoveAll
(он немного медленнее в моем профилировании), потому что он выделяет новый список, я фигурирую, но довольно очень незначительно):
public static void MethodD()
{
var originalList = new List<int>(Enumerable.Range(1, 1000));
var list = new List<int>(originalList.Count);
foreach (var item in originalList)
{
if (item <= 500)
list.Add(item);
}
list.Capacity = list.Count;
}
Вам просто нужно отменить условие.
Я не предлагаю этот метод, просто доказывая, что foreach
не обязательно медленнее, чем for
Ответ 5
Другим способом может быть выбор списка, установка элемента в значение null, а затем удаление linq в (item = > null), как уже говорилось, это зависит от вас, чтобы выбрать способ, который вам подходит лучше