.NET - удалить из списка<T> в цикле 'foreach'
У меня есть код, который я хочу выглядеть следующим образом:
List<Type> Os;
...
foreach (Type o in Os)
if (o.cond)
return; // Quitting early is important for my case!
else
Os.Remove(o);
... // Other code
Это не работает, потому что вы не можете удалить из списка, когда вы находитесь внутри foreach
цикл по этому списку:
Есть ли общий способ решения проблемы?
Я могу переключиться на другой тип, если это необходимо.
Вариант 2:
List<Type> Os;
...
while (Os.Count != 0)
if (Os[0].cond)
return;
else
Os.RemoveAt(0);
... // Other code
Безобразно, но это должно работать.
17 ответов
Вы действительно должны сделать это в течение foreach
цикл?
Это приведет к тем же результатам, что и ваши примеры, т.е. удалит все элементы из списка вплоть до первого элемента, соответствующего условию (или удалит все элементы, если ни один из них не соответствует условию).
int index = Os.FindIndex(x => x.cond);
if (index > 0)
Os.RemoveRange(0, index);
else if (index == -1)
Os.Clear();
Вы можете перебрать список в обратном направлении:
for (int i = myList.Count - 1; i >= 0; i--)
{
if (whatever) myList.RemoveAt(i);
}
В ответ на ваш комментарий о желании выйти, когда вы обнаружите элемент, который НЕ удаляете, лучшим решением будет просто использование цикла while.
Вы никогда не должны удалять что-либо из коллекции, которую вы перебираете внутри цикла foreach. Это в основном как пилить ветку, на которой ты сидишь.
Используйте альтернативу. Это путь.
Я программист на Java, но что-то вроде этого работает:
List<Type> Os;
List<Type> Temp;
...
foreach (Type o in Os)
if (o.cond)
Temp.add(o);
Os.removeAll(Temp);
У меня просто была эта проблема с моей аналитической библиотекой. Я попробовал это:
for (int i = 0; i < list.Count; i++)
{
if (/*condition*/)
{
list.RemoveAt(i);
i--;
}
}
Это довольно просто, но я не думал ни о каком переломном моменте.
Я знаю, что вы просили что-то еще, но если вы хотите условно удалить группу элементов, вы можете использовать лямбда-выражение:
Os.RemoveAll(o => !o.cond);
Вот самое простое решение с самым простым ПОЧЕМУ
ПРОБЛЕМА:
Как правило, мы удаляем из исходного списка, это создает проблему поддержания счетчика списка и местоположения итератора.
List<Type> Os = ....;
Os.ForEach(
delegate(Type o) {
if(!o.cond) Os.Remove(o);
}
);
РЕШЕНИЕ - LINQ.ForEach
:
Обратите внимание, все, что я добавил, было ToList()
, Это создает новый список, для которого вы выполняете ForEach, поэтому вы можете удалить свой оригинальный список, продолжая при этом перебирать весь список.
List<Type> Os = ....;
Os.ToList().ForEach(
delegate(Type o) {
if(!o.cond) Os.Remove(o);
}
);
РЕШЕНИЕ - Обычный foreach
:
Эта техника также работает для обычных foreach
заявления.
List<Type> Os = ....;
foreach(Type o in Os.ToList()) {
if(!o.cond) Os.Remove(o);
}
Обратите внимание, что это решение не будет работать, если ваш оригинальный список содержит struct
элемент.
Я бы попытался найти индекс первого элемента, который не удовлетворяет предикату, и выполнить для него RemoveRange(0, index). Если ничего другого, должно быть меньше Удалить звонки.
Обновление: добавлено для полноты
Как ответили несколько человек, вы не должны изменять коллекцию во время ее итерации с помощью GetEnumerator() (пример foreach
). Фреймворк мешает вам сделать это, создав исключение. Общая формулировка этого заключается в том, чтобы выполнить итерацию "вручную" с for
(см. другие ответы). Будьте осторожны с индексом, чтобы не пропустить элементы и не переоценить один и тот же дважды (используя i--
или итерация назад).
Однако для вашего конкретного случая мы можем оптимизировать операцию (ы) удаления... оригинальный ответ ниже.
Если вы хотите удалить все элементы до тех пор, пока один из них не удовлетворяет заданному условию (это то, что делает ваш код), вы можете сделать это:
bool exitCondition;
while(list.Count > 0 && !(exitCondition = list[0].Condition))
list.RemoveAt(0);
Или, если вы хотите использовать одну операцию удаления:
SomeType exitCondition;
int index = list.FindIndex(i => i.Condition);
if(index < 0)
list.Clear();
else
{
exitCondition = list[0].State;
list.RemoveRange(0, count);
}
Примечание: так как я предполагаю, что item.Condition
является bool
, Я использую item.State
сохранить условие выхода.
Обновление: добавлена проверка границ и сохранение условия выхода в обоих примерах
Вы можете сделать это с Linq
MyList = MyList.Where(x=>(someCondition(x)==true)).ToList()
Это хорошо обсуждается в разделе "Удаление элементов в списке во время итерации по нему".
Они предлагают:
for(int i = 0; i < count; i++)
{
int elementToRemove = list.Find(<Predicate to find the element>);
list.Remove(elementToRemove);
}
Если вы знаете, что ваш список не очень большой, вы можете использовать
foreach (Type o in new List<Type>(Os))
....
который создаст временную копию списка. Ваш вызов remove() не будет мешать итератору.
Решение Anzurio, вероятно, является наиболее простым, но вот еще одно простое решение, если вы не возражаете добавить несколько интерфейсов / классов в свою библиотеку утилит.
Вы можете написать это так
List<Type> Os;
...
var en = Os.GetRemovableEnumerator();
while (en.MoveNext())
{
if (en.Current.Cond)
en.Remove();
}
Поместите следующую инфраструктуру, вдохновленную JavaIterator<T>.remove
в вашу служебную библиотеку:
static class Extensions
{
public static IRemovableEnumerator<T> GetRemovableEnumerator<T>(this IList<T> l)
{
return new ListRemovableEnumerator<T>(l);
}
}
interface IRemovableEnumerator<T> : IEnumerator<T>
{
void Remove();
}
class ListRemovableEnumerator<T> : IRemovableEnumerator<T>
{
private readonly IList<T> _list;
private int _count;
private int _index;
public ListRemovableEnumerator(IList<T> list)
{
_list = list;
_count = list.Count;
_index = -1;
}
private void ThrowOnModification()
{
if (_list.Count != _count)
throw new InvalidOperationException("List was modified after creation of enumerator");
}
public void Dispose()
{
}
public bool MoveNext()
{
ThrowOnModification();
if (_index + 1 == _count)
return false;
_index++;
return true;
}
public void Reset()
{
ThrowOnModification();
_index = -1;
}
object IEnumerator.Current
{
get { return Current; }
}
public T Current
{
get { return _list[_index]; }
}
public void Remove()
{
ThrowOnModification();
_list.RemoveAt(_index);
_index--;
_count--;
}
}
Смотреть на Enumerable.SkipWhile()
Enumerable.SkipWhile( x => condition).ToList()
Как правило, не изменяя список, делает жизнь намного проще.:)
Добавьте элемент для удаления в список, а затем удалите эти элементы с помощью RemoveAll
:
List<Type> Os;
List<Type> OsToRemove=new List<Type>();
...
foreach (Type o in Os){
if (o.cond)
return;
else
OsToRemove.Add(o);
}
Os.RemoveAll(o => OsToRemove.Contains(o));
У меня была та же проблема, и я решил ее с помощью следующего:
foreach (Type o in (new List(Os)))
{
if (something)
Os.Remove(o);
}
Он перебирает копию списка и удаляет из исходного списка.