Inteligentny sposób usuwania elementów z List <T> podczas wyliczania w C #

87

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:

  1. 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.

  2. 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.

  3. 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?

  4. Powiedziano mi, że dostęp myIntCollection.ToArray()zamiast myIntCollectionrozwią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 (

John Stock
źródło
Ewentualna przydatna informacja dla kogoś innego: Błąd unikania kolekcji został zmodyfikowany (hermetyzacja wzorca 1)
George Duckett
Na marginesie: listy oszczędzają dużo czasu (zazwyczaj O (N), gdzie N to długość listy) przenosząc wartości. Jeśli naprawdę potrzebny jest wydajny dostęp losowy, możliwe jest usunięcie w O (log N), używając zbalansowanego drzewa binarnego zawierającego liczbę węzłów w poddrzewie, którego jest korzeniem. Jest to BST, którego klucz (indeks w sekwencji) jest implikowany.
Palec
Zobacz odpowiedź: stackoverflow.com/questions/7193294/…
Dabbas

Odpowiedzi:

196

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 forlub whilepę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));
dlev
źródło
Jeśli chodzi o twoją odpowiedź: muszę natychmiast usunąć elementy podczas przetwarzania tego elementu, a nie po przetworzeniu całej pętli. Rozwiązaniem, którego używam, jest NULL wszystkie elementy, które chcę natychmiast usunąć, a następnie je usunąć. To nie jest idealne rozwiązanie, ponieważ muszę wszędzie sprawdzać NULL, ale DZIAŁA.
John Stock
Zastanawiasz się, czy 'elem' nie jest intem, nie możemy użyć RemoveAll w ten sposób, że jest obecny w edytowanym kodzie odpowiedzi.
Użytkownik M
22

Jeśli musisz zarówno wyliczyć a, jak List<T>i usunąć z niego, sugeruję po prostu użycie whilepętli zamiast aforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}
JaredPar
źródło
Moim zdaniem powinna to być akceptowana odpowiedź. Pozwala to na rozważenie pozostałych pozycji na liście bez ponownego powtarzania krótkiej listy elementów do usunięcia.
Slvrfn
13

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);
        }
    }
}
D-Jones
źródło
Świetny pomysł na „.ToList ()”! Prosty slapper, a także działa w przypadkach, gdy nie używasz bezpośrednio mety "Usuń ... ()".
galaxis
1
Jednak bardzo nieefektywne.
nawfal
8

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ść iza 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 RemoveAlltak, jak zasugerował dlev.

Justin
źródło
Działa jednak tylko wtedy, gdy usuwasz bieżący element. Jeśli usuwasz dowolny element, będziesz musiał sprawdzić, czy jego indeks był na poziomie / przed, czy po bieżącym indeksie, aby zdecydować, czy to zrobić --i.
CompuChip
Pierwotne pytanie nie wyjaśniało, że należy wspierać usuwanie innych elementów niż obecny, @CompuChip. Ta odpowiedź nie zmieniła się od czasu jej wyjaśnienia.
Palec
@Palec, rozumiem, stąd mój komentarz.
CompuChip
5

W trakcie wyliczania listy dodaj tę, którą chcesz ZACHOWAĆ do nowej listy. Następnie przypisz nową listę do plikumyIntCollection

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;
James Curran
źródło
3

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);
}
Cristian Voiculescu
źródło
1

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;
    }
kvb
źródło
0

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.
}
Olaf
źródło
W obecnym języku C # można go przepisać tak, jak w foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }przypadku każdego wyliczalnego, a List<T>konkretnie obsługuje tę metodę nawet w .NET 2.0.
Palec
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;
}
Will Calderwood
źródło
0

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.

Markus Knappen Johansson
źródło