Manière intelligente de supprimer des éléments d'un List <T> lors de l'énumération en C #


87

J'ai le cas classique d'essayer de supprimer un élément d'une collection tout en l'énumérant dans une boucle:

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

Au début de l'itération après une modification, un InvalidOperationExceptionest levé, car les énumérateurs n'aiment pas quand la collection sous-jacente change.

Je dois apporter des modifications à la collection lors de l'itération. Il existe de nombreux modèles qui peuvent être utilisés pour éviter cela , mais aucun d'entre eux ne semble avoir une bonne solution:

  1. Ne supprimez pas à l'intérieur de cette boucle, conservez plutôt une «liste de suppression» séparée, que vous traitez après la boucle principale.

    C'est normalement une bonne solution, mais dans mon cas, j'ai besoin que l'élément disparaisse instantanément car «attendre» qu'après la boucle principale pour vraiment supprimer l'élément change le flux logique de mon code.

  2. Au lieu de supprimer l'élément, définissez simplement un indicateur sur l'élément et marquez-le comme inactif. Ajoutez ensuite la fonctionnalité du modèle 1 pour nettoyer la liste.

    Ce serait travailler pour tous mes besoins, mais cela signifie que beaucoup de code à changer afin de vérifier le drapeau inactif chaque fois qu'un élément est accessible. C'est beaucoup trop d'administration à mon goût.

  3. Incorporez d'une manière ou d'une autre les idées du modèle 2 dans une classe qui dérive de List<T>. Cette super liste gérera l'indicateur inactif, la suppression des objets après coup et n'exposera pas non plus les éléments marqués comme inactifs aux consommateurs d'énumération. Fondamentalement, il encapsule simplement toutes les idées du modèle 2 (et par la suite du modèle 1).

    Une classe comme celle-ci existe-t-elle? Quelqu'un a-t-il un code pour cela? Ou y a-t-il un meilleur moyen?

  4. On m'a dit qu'accéder myIntCollection.ToArray()au lieu de myIntCollectionrésoudrait le problème et me permettrait de supprimer à l'intérieur de la boucle.

    Cela me semble être un mauvais modèle de conception, ou peut-être que ça va?

Détails:

  • La liste contiendra de nombreux éléments et je n'en supprimerai que quelques-uns.

  • À l'intérieur de la boucle, je vais faire toutes sortes de processus, ajouter, supprimer, etc., donc la solution doit être assez générique.

  • L'élément que je dois supprimer n'est peut- être pas l'élément actuel de la boucle. Par exemple, je peux être sur l'élément 10 d'une boucle de 30 éléments et avoir besoin de supprimer l'élément 6 ou l'élément 26. Marcher en arrière dans le tableau ne fonctionnera plus à cause de cela. ; o (


Informations utiles possibles pour quelqu'un d'autre: erreur Éviter la collection a été modifiée (une encapsulation du modèle 1)
George Duckett

Remarque: les listes permettent de gagner beaucoup de temps (généralement O (N), où N est la longueur de la liste) en déplaçant les valeurs. Si un accès aléatoire efficace est vraiment nécessaire, il est possible de réaliser des suppressions dans O (log N), en utilisant un arbre binaire équilibré contenant le nombre de nœuds dans le sous-arbre dont il est la racine. C'est un BST dont la clé (l'index dans la séquence) est implicite.
Palec

Veuillez voir la réponse: stackoverflow.com/questions/7193294/…
Dabbas

Réponses:


196

La meilleure solution est généralement d'utiliser la RemoveAll()méthode:

myList.RemoveAll(x => x.SomeProp == "SomeValue");

Ou, si vous avez besoin de certains éléments supprimés:

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

Cela suppose que votre boucle est uniquement destinée à des fins de suppression, bien sûr. Si vous avez besoin de traitement supplémentaire, la meilleure méthode est généralement d'utiliser unfor ou whileboucle, car vous n'êtes pas en utilisant un recenseur:

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

Revenir en arrière garantit que vous ne sautez aucun élément.

Réponse à la modification :

Si vous allez supprimer des éléments apparemment arbitraires, la méthode la plus simple peut être de simplement garder une trace des éléments que vous souhaitez supprimer, puis de les supprimer tous en même temps. Quelque chose comme ça:

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));

Concernant votre réponse: je dois supprimer les éléments instantanément pendant le traitement de cet élément et non après que toute la boucle a été traitée. La solution que j'utilise est de NULL tous les éléments que je souhaite supprimer instantanément et de les supprimer par la suite. Ce n'est pas une solution idéale car je dois vérifier NULL partout, mais cela fonctionne.
John Stock

Vous vous demandez si 'elem' n'est pas un int, alors nous ne pouvons pas utiliser RemoveAll de cette façon, il est présent sur le code de réponse modifié.
Utilisateur M

22

Si vous devez à la fois énumérer un List<T>et le supprimer, je suggère simplement d'utiliser une whileboucle au lieu d'unforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

Cela devrait être la réponse acceptée à mon avis. Cela vous permet de prendre en compte le reste des éléments de votre liste, sans réitérer une liste restreinte d'éléments à supprimer.
Slvrfn

13

Je sais que cet article est ancien, mais j'ai pensé partager ce qui a fonctionné pour moi.

Créez une copie de la liste pour l'énumération, puis dans le pour chaque boucle, vous pouvez traiter les valeurs copiées et supprimer / ajouter / quoi que ce soit avec la liste source.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

Excellente idée sur le ".ToList ()"! Simple head-slapper, et fonctionne également dans les cas où vous n'utilisez pas directement le stock "Remove ... ()" meths.
galaxis

1
Très inefficace cependant.
nawfal le

8

Lorsque vous avez besoin d'itérer une liste et que vous pouvez la modifier pendant la boucle, il vaut mieux utiliser une boucle for:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

Bien sûr il faut faire attention, par exemple je décrémente i chaque fois qu'un élément est supprimé, sinon nous sauterons des entrées (une alternative est de revenir en arrière dans la liste).

Si vous avez Linq, utilisez simplement RemoveAllcomme dlev l'a suggéré.


Cela ne fonctionne que lorsque vous supprimez l'élément actuel. Si vous supprimez un élément arbitraire, vous devrez vérifier si son index était à / avant ou après l'index actuel pour décider de le faire --i.
CompuChip

La question d'origine ne précisait pas clairement que la suppression d'autres éléments que l'actuel devait être prise en charge, @CompuChip. Cette réponse n'a pas changé depuis qu'elle a été clarifiée.
Palec

@Palec, je comprends, d'où mon commentaire.
CompuChip

5

Au fur et à mesure que vous énumérez la liste, ajoutez celle que vous souhaitez GARDER dans une nouvelle liste. Ensuite, attribuez la nouvelle liste aumyIntCollection

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;

3

Ajoutons votre code:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

Si vous souhaitez modifier la liste pendant que vous êtes dans un foreach, vous devez taper .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

1

Pour ceux que cela peut aider, j'ai écrit cette méthode d'extension pour supprimer les éléments correspondant au prédicat et renvoyer la liste des éléments supprimés.

    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;
    }

0

Que diriez-vous

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

Dans C # actuel, il peut être réécrit comme foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }pour n'importe quel énumérable et List<T>prend spécifiquement en charge cette méthode, même dans .NET 2.0.
Palec

0

Si vous êtes intéressé par la haute performance, vous pouvez utiliser deux listes. Ce qui suit minimise le garbage collection, maximise la localité de la mémoire et ne supprime jamais réellement un élément d'une liste, ce qui est très inefficace s'il ne s'agit pas du dernier élément.

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;
}

0

Je viens de penser que je vais partager ma solution à un problème similaire où je devais supprimer des éléments d'une liste tout en les traitant.

Donc, fondamentalement, "foreach" supprimera l'élément de la liste une fois qu'il a été itéré.

Mon 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));

J'ai résolu cela avec une méthode d'extension "PopForEach" qui effectuera une action puis supprimera l'élément de la liste.

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);
        }
    }
}

J'espère que cela peut être utile à n'importe qui.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.