web-dev-qa-db-fra.com

Méthode intelligente pour supprimer des éléments d'une liste <T> lors d'une énumération en C #

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 la modification, une variable InvalidOperationException est générée, car les énumérateurs n'aiment pas le changement de la collection sous-jacente.

Je dois apporter des modifications à la collection en effectuant une itération. Il y a beaucoup de schémas qui peuvent être utilisés pour éviter ceci , mais aucun d'entre eux ne semble avoir de bonne solution:

  1. Ne supprimez pas cette boucle, conservez une «liste de suppression» distincte, 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» après La boucle principale pour réellement supprimer l’élément modifie 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 motif 1 pour nettoyer la liste. 

    Ceci fonctionnerait pour tous mes besoins, mais cela signifie qu'un lot de code devra être modifié afin de vérifier l'indicateur inactif à chaque accès à un élément. C'est beaucoup trop d'administration à mon goût.

  3. En quelque sorte, incorporer les idées du motif 2 dans une classe dérivée de List<T>. Cette super liste traitera l'indicateur inactif, la suppression d'objets après le fait et n'exposera pas non plus les éléments marqués comme inactifs aux utilisateurs de l'énumération. Fondamentalement, il encapsule toutes les idées du motif 2 (et ensuite du motif 1). 

    Une classe comme celle-ci existe-t-elle? Quelqu'un at-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 myIntCollection résoudrait le problème et me permettrait de supprimer à l'intérieur de la boucle. 

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

Détails:

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

  • Dans 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 ne peut pas être l'élément actuel de la boucle. Par exemple, je peux être sur l’article 10 d’une boucle de 30 éléments et avoir besoin de supprimer l’article 6 ou l’article 26. La marche arrière du tableau ne fonctionnera plus. ; o (

78
John Stock

La meilleure solution consiste généralement à utiliser la méthode RemoveAll() :

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. Si vous do avez besoin d'un traitement supplémentaire, la meilleure méthode consiste généralement à utiliser une boucle for ou while, car vous n'utilisez pas d'énumérateur:

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 à Edit:

Si vous souhaitez supprimer des éléments apparemment arbitraires, la méthode la plus simple consiste peut-être simplement à suivre les éléments que vous souhaitez supprimer, puis à 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));
182
dlev

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

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

Lorsque vous avez besoin de parcourir une liste et de la modifier pendant la boucle, il est préférable d'utiliser une boucle for:

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

Bien sûr, vous devez faire attention, par exemple, je décrémente i chaque fois qu'un élément est supprimé, sinon nous ignorerons les entrées (une autre solution consiste à revenir en arrière dans la liste).

Si vous avez Linq, vous devriez simplement utiliser RemoveAll comme l'a suggéré dlev.

8
Justin

Je sais que ce post est vieux, mais je pensais partager ce qui a fonctionné pour moi. 

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

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

Au fur et à mesure que vous énumérez la liste, ajoutez celui que vous souhaitez GARDER à une nouvelle liste. Ensuite, affectez la nouvelle liste à la 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;
4
James Curran

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 lorsque vous vous trouvez 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);
}
3

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

Si la haute performance vous intéresse, vous pouvez utiliser deux listes. Ce qui suit minimise la récupération de place, maximise l'emplacement de la mémoire et ne supprime jamais 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
Will Calderwood