Mam klasyczny przypadek próby usunięcia elementu z kolekcji podczas wyliczania go w pętli:
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.
}
Na początku iteracji po zmianie jest generowany an InvalidOperationException
, ponieważ moduł wyliczający nie lubi, gdy zmienia się podstawowa kolekcja.
Muszę wprowadzić zmiany w kolekcji podczas iteracji. Istnieje wiele wzorców, których można użyć, aby tego uniknąć , ale żaden z nich nie wydaje się mieć dobrego rozwiązania:
Nie usuwaj wewnątrz tej pętli, zamiast tego zachowaj oddzielną listę usuwania, którą przetwarzasz po głównej pętli.
Zwykle jest to dobre rozwiązanie, ale w moim przypadku potrzebuję, aby element zniknął natychmiast, ponieważ „czekam”, aż po zakończeniu głównej pętli, aby naprawdę usunąć element, zmieni się logika przepływu mojego kodu.
Zamiast usuwać element, po prostu ustaw flagę na elemencie i oznacz go jako nieaktywny. Następnie dodaj funkcjonalność wzorca 1, aby wyczyścić listę.
To będzie pracować dla wszystkich moich potrzeb, ale to oznacza, że wiele z kodem będzie musiał zmienić w celu sprawdzenia nieaktywnej flagę za każdym razem element jest dostęp. Jak na mój gust to zdecydowanie za dużo administracji.
W jakiś sposób włącz idee wzorca 2 do klasy, z której pochodzi
List<T>
. Ta Superlista będzie obsługiwać flagę nieaktywną, usuwanie obiektów po fakcie, a także nie będzie ujawniać elementów oznaczonych jako nieaktywne konsumentom wyliczenia. Zasadniczo po prostu zawiera wszystkie idee wzorca 2 (a następnie wzorca 1).Czy istnieje taka klasa? Czy ktoś ma do tego kod? Czy jest jakiś lepszy sposób?
Powiedziano mi, że dostęp
myIntCollection.ToArray()
zamiastmyIntCollection
rozwiązania rozwiąże problem i pozwoli mi usunąć wewnątrz pętli.Wydaje mi się, że to zły wzorzec projektowy, a może jest w porządku?
Detale:
Lista będzie zawierała wiele pozycji, a usunę tylko część z nich.
Wewnątrz pętli będę wykonywać różnego rodzaju procesy, dodawać, usuwać itp., Więc rozwiązanie musi być dość ogólne.
Element, który muszę usunąć, może nie być bieżącym elementem w pętli. Na przykład, mogę być na pozycji 10 w pętli zawierającej 30 pozycji i muszę usunąć pozycję 6 lub pozycję 26. Przechodzenie wstecz przez tablicę nie będzie już działać z tego powodu. ; o (
źródło
Odpowiedzi:
Najlepszym rozwiązaniem jest zazwyczaj użycie
RemoveAll()
metody:myList.RemoveAll(x => x.SomeProp == "SomeValue");
Lub, jeśli chcesz usunąć niektóre elementy:
MyListType[] elems = new[] { elem1, elem2 }; myList.RemoveAll(x => elems.Contains(x));
Zakłada się oczywiście, że twoja pętla jest przeznaczona wyłącznie do usuwania. Jeśli zrobić konieczność dodatkowej obróbki, to najlepszym sposobem jest zwykle używanie
for
lubwhile
pętlę, gdyż wtedy nie używasz wyliczający:for (int i = myList.Count - 1; i >= 0; i--) { // Do processing here, then... if (shouldRemoveCondition) { myList.RemoveAt(i); } }
Cofanie się do tyłu gwarantuje, że nie pominiesz żadnych elementów.
Odpowiedź na edycję :
Jeśli chcesz usunąć pozornie dowolne elementy, najłatwiejszą metodą może być po prostu śledzenie elementów, które chcesz usunąć, a następnie usunięcie ich wszystkich naraz. Coś takiego:
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));
źródło
Jeśli musisz zarówno wyliczyć a, jak
List<T>
i usunąć z niego, sugeruję po prostu użyciewhile
pętli zamiast aforeach
var index = 0; while (index < myList.Count) { if (someCondition(myList[index])) { myList.RemoveAt(index); } else { index++; } }
źródło
Wiem, że ten post jest stary, ale pomyślałem, że podzielę się tym, co zadziałało.
Utwórz kopię listy do wyliczenia, a następnie w pętli for each możesz przetwarzać skopiowane wartości i usuwać / dodawać / cokolwiek z listą źródłową.
private void ProcessAndRemove(IList<Item> list) { foreach (var item in list.ToList()) { if (item.DeterminingFactor > 10) { list.Remove(item); } } }
źródło
Kiedy potrzebujesz iterować listę i możesz ją zmodyfikować podczas pętli, lepiej użyć pętli for:
for (int i = 0; i < myIntCollection.Count; i++) { if (myIntCollection[i] == 42) { myIntCollection.Remove(i); i--; } }
Oczywiście musisz być ostrożny, na przykład zmniejszam wartość
i
za każdym razem, gdy usuwany jest element, ponieważ w przeciwnym razie pominiemy wpisy (alternatywą jest cofnięcie się po liście).Jeśli masz Linq, powinieneś użyć go
RemoveAll
tak, jak zasugerował dlev.źródło
--i
.W trakcie wyliczania listy dodaj tę, którą chcesz ZACHOWAĆ do nowej listy. Następnie przypisz nową listę do pliku
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;
źródło
Dodajmy kod:
List<int> myIntCollection=new List<int>(); myIntCollection.Add(42); myIntCollection.Add(12); myIntCollection.Add(96); myIntCollection.Add(25);
Jeśli chcesz zmienić listę, gdy jesteś w foreach, musisz wpisać
.ToList()
foreach(int i in myIntCollection.ToList()) { if (i == 42) myIntCollection.Remove(96); if (i == 25) myIntCollection.Remove(42); }
źródło
Dla tych, którzy mogą pomóc, napisałem tę metodę rozszerzenia, aby usunąć elementy pasujące do predykatu i zwrócić listę usuniętych elementów.
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; }
źródło
Co powiesz na
int[] tmp = new int[myIntCollection.Count ()]; myIntCollection.CopyTo(tmp); foreach(int i in tmp) { myIntCollection.Remove(42); //The error is no longer here. }
źródło
foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }
przypadku każdego wyliczalnego, aList<T>
konkretnie obsługuje tę metodę nawet w .NET 2.0.Jeśli interesuje Cię wysoka wydajność, możesz skorzystać z dwóch list. Poniższe działania minimalizują wyrzucanie elementów bezużytecznych, maksymalizują lokalność pamięci i nigdy nie usuwają elementu z listy, co jest bardzo nieefektywne, jeśli nie jest ostatnim elementem.
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; }
źródło
Pomyślałem, że podzielę się moim rozwiązaniem podobnego problemu, w którym muszę usunąć elementy z listy podczas ich przetwarzania.
Zasadniczo więc „foreach” usunie pozycję z listy po jej iteracji.
Mój test:
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));
Rozwiązałem to za pomocą metody rozszerzającej „PopForEach”, która wykona akcję, a następnie usunie element z listy.
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); } } }
Mam nadzieję, że to może być pomocne dla każdego.
źródło