web-dev-qa-db-fra.com

Supprimer/Ajouter aux éléments d'une liste tout en l'itérant

Premièrement, je sais que ce n’est pas possible immédiatement, pour des raisons évidentes.

foreach(string item in myListOfStrings) {
    myListOfStrings.Remove(item);
}

La capture ci-dessus est l'une des choses les plus horribles que j'ai jamais vues. Alors, comment y parvenez-vous alors? Vous pouvez parcourir la liste en arrière en utilisant for, mais je n'aime pas cette solution non plus.

Ce que je me demande, c'est: existe-t-il une méthode/extension qui renvoie un IEnumerable à partir de la liste actuelle, quelque chose comme une copie flottante? LINQ a de nombreuses méthodes d'extension qui font exactement cela, mais vous devez toujours en faire quelque chose, comme le filtrage (où, prenez ...).

Je suis impatient de voir quelque chose comme ça:

foreach(string item in myListOfStrings.Shadow()) {
   myListOfStrings.Remove(item);
}

où as .Shadow () est:

public static IEnumerable<T> Shadow<T>(this IEnumerable<T> source) {
    return new IEnumerable<T>(source);
    // or return source.Copy()
    // or return source.TakeAll();
}

Exemple

foreach(ResponseFlags flag in responseFlagsList.Shadow()) {
    switch(flag) {
        case ResponseFlags.Case1:
            ...
        case ResponseFlags.Case2:
            ...
    }
    ...
    this.InvokeSomeVoidEvent(flag)
    responseFlagsList.Remove(flag);
}

Solution

Voici comment je l'ai résolu, et cela fonctionne comme un charme:

public static IEnumerable<T> Shadow<T>(this IEnumerable<T> source) where T: new() {
    foreach(T item in source)
        yield return item;
}

Ce n'est pas si rapide (évidemment), mais c'est sûr et c'est exactement ce que j'avais l'intention de faire.

10
Atrotygma

La suppression de plusieurs éléments d'une liste 1 par 1 est un anti-motif C # en raison de la façon dont les listes sont implémentées.

Bien sûr, cela peut être fait avec une boucle for (au lieu de foreach). Ou cela peut être fait en faisant une copie de la liste. Mais voici pourquoi ne devrait pas être fait . Sur une liste de 100 000 entiers aléatoires, cela prend 2500 ms sur ma machine:

       foreach (var x in listA.ToList())
            if (x % 2 == 0)
                listA.Remove(x);

et cela prend 1250 ms:

        for (int i = 0; i < listA.Count; i++)
            if (listA[i] % 2 == 0)
                listA.RemoveAt(i--);

alors que ces deux prennent respectivement 5 et 2 ms:

        listB = listB.Where(x => x % 2 != 0).ToList();

        listB.RemoveAll(x => x % 2 == 0);

En effet, lorsque vous supprimez un élément d'une liste, vous supprimez en fait un tableau , et il s'agit de O(N) heure, car vous devez déplacer chaque élément after l'élément supprimé une position à gauche. En moyenne, ce sera N/2 éléments.

Supprimer (élément) doit également rechercher l'élément avant de le supprimer. Donc Supprimer (élément) prendra toujours toujours N étapes - elementindex étapes pour trouver l'élément, N - elementindex étapes pour l'enlever - au total, N étapes.

RemoveAt (index) ne doit pas nécessairement trouver l'élément, mais il doit toujours déplacer le tableau sous-jacent. En moyenne, RemoveAt correspond à N/2 étapes.

Le résultat final est la complexité O (N ^ 2) dans tous les cas, car vous supprimez jusqu'à N éléments.

Au lieu de cela, vous devriez utiliser Linq, qui modifiera la liste entière en O(N) heure, ou rouler la vôtre, mais vous ne devriez pas utiliser Remove (ou RemoveAt) dans une boucle.

25
svinja

Pourquoi ne pas simplement faire:

foreach(string item in myListOfStrings.ToList()) 
{
    myListOfStrings.Remove(item);
}

Pour créer une copie de l'original et l'utiliser pour l'itération, puis supprimez l'existant.

Si vous avez vraiment besoin de votre méthode d'extension, vous pouvez peut-être créer quelque chose de plus lisible pour l'utilisateur, tel que:

 public static IEnumerable<T> Shadow<T>(this IEnumerable<T> items)
 {
     if (items == null)
        throw new NullReferenceException("Items cannot be null");

     List<T> list = new List<T>();
     foreach (var item in items)
     {
         list.Add(item);
     }
     return list;
 }

Ce qui est essentiellement identique à .ToList().

Appel:

foreach(string item in myListOfStrings.Shadow())

8
DGibbs

Vous devez créer une copie de la liste originale en effectuant la répétition suivante:

        var myListOfStrings = new List<string>();

        myListOfStrings.Add("1");
        myListOfStrings.Add("2");
        myListOfStrings.Add("3");
        myListOfStrings.Add("4");
        myListOfStrings.Add("5");

        foreach (string item in myListOfStrings.ToList())
        {
            myListOfStrings.Remove(item);
        }
3
Azhar Khorasany

Votre exemple supprime tous les éléments de la chaîne, il est donc équivalent à:

myListOfStrings.Clear();

C'est aussi équivalent à:

myListOfStrings.RemoveAll(x => true); // Empties myListOfStrings

Mais ce que je pense que vous recherchez, c’est un moyen de supprimer les éléments pour lesquels un prédicat est vrai - c’est ce que fait RemoveAll().

Pour que vous puissiez écrire, par exemple:

myListOfStrings.RemoveAll(x => x == "TEST"); // Modifies myListOfStrings

Ou utilisez n'importe quel autre prédicat.

Cependant, cela change la liste ORIGINAL; Si vous souhaitez simplement une copie de la liste avec certains éléments supprimés, vous pouvez simplement utiliser Linq normal:

// Note != instead of == as used in Removeall(), 
// because the logic here is reversed.

var filteredList = myListOfStrings.Where(x => x != "TEST").ToList(); 
3
Matthew Watson

Vous n'avez pas les méthodes d'extension LINQ pour cela - vous pouvez créer une nouvelle liste explicitement, comme ceci:

foreach(string item in new List<string>(myListOfStrings)) {
    myListOfStrings.Remove(item);
}
3
dasblinkenlight

Reprenant la réponse de svinja Je pense que le moyen le plus efficace de résoudre ce problème consiste à:

for (int i = 0; i < listA.Count;) {
    if (listA[i] % 2 == 0)
        listA.RemoveAt(i);
    else
        i++;
}

Il améliore la réponse en supprimant les sommes et les soustractions inutiles.

0