Comment supprimer des éléments d'une liste générique tout en itérant dessus?


451

Je cherche un meilleur modèle pour travailler avec une liste d'éléments dont chacun a besoin d'être traité, puis en fonction du résultat sont supprimés de la liste.

Vous ne pouvez pas utiliser à l' .Remove(element)intérieur d'un foreach (var element in X)(car cela entraîne une Collection was modified; enumeration operation may not execute.exception) ... vous ne pouvez pas non plus utiliser for (int i = 0; i < elements.Count(); i++)et.RemoveAt(i) parce qu'il perturbe votre position actuelle dans la collection par rapport à i.

Existe-t-il une manière élégante de procéder?

Réponses:


734

Répétez votre liste en sens inverse avec une boucle for:

for (int i = safePendingList.Count - 1; i >= 0; i--)
{
    // some code
    // safePendingList.RemoveAt(i);
}

Exemple:

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i] > 5)
        list.RemoveAt(i);
}
list.ForEach(i => Console.WriteLine(i));

Alternativement, vous pouvez utiliser la méthode RemoveAll avec un prédicat pour tester par rapport:

safePendingList.RemoveAll(item => item.Value == someValue);

Voici un exemple simplifié pour démontrer:

var list = new List<int>(Enumerable.Range(1, 10));
Console.WriteLine("Before:");
list.ForEach(i => Console.WriteLine(i));
list.RemoveAll(i => i > 5);
Console.WriteLine("After:");
list.ForEach(i => Console.WriteLine(i));

19
Pour ceux qui viennent de Java, la liste de C # est comme ArrayList dans la mesure où l'insertion / suppression est O (n) et la récupération via un index est O (1). Ce n'est pas une liste chaînée traditionnelle. Il semble un peu malheureux que C # utilise le mot "Liste" pour décrire cette structure de données car il évoque la liste chaînée classique.
Jarrod Smith

79
Rien dans le nom «List» ne dit «LinkedList». Les gens venant d'autres langages que Java pourraient être confus quand ce serait une liste chaînée.
GolezTrol

5
Je me suis retrouvé ici grâce à une recherche sur vb.net, donc au cas où quelqu'un voudrait la syntaxe équivalente à vb.net pour RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder

1
Cela fonctionne également en Java avec un minimum de modifications: .size()au lieu de .Countet .remove(i)au lieu de .removeAt(i). Intelligent - merci!
Automne Leonard

2
J'ai fait un petit test de performance et il s'est avéré que cela RemoveAll()prend trois fois plus de temps que la forboucle arrière . Je reste donc fidèle à la boucle, du moins dans les sections où cela compte.
Crusha K.Rool

84

Une solution simple et directe:

Utilisez une boucle for for standard fonctionnant à l' envers sur votre collection et RemoveAt(i)pour supprimer des éléments.


2
Sachez que supprimer des éléments un par un n'est pas efficace si votre liste contient de nombreux éléments. Il a le potentiel d'être O (n ^ 2). Imaginez une liste avec deux milliards d'éléments où il se trouve que le premier milliard d'éléments finit tous par être supprimé. Chaque suppression oblige à copier tous les éléments ultérieurs, vous finissez donc par copier un milliard d'éléments un milliard de fois chacun. Ce n'est pas à cause de l'itération inverse, c'est à cause de la suppression une à la fois. RemoveAll garantit que chaque élément est copié au plus une fois afin qu'il soit linéaire. L'élimination une à la fois peut être un milliard de fois plus lente. O (n) contre O (n ^ 2).
Bruce Dawson

71

L'itération inverse doit être la première chose à laquelle vous pensez lorsque vous souhaitez supprimer des éléments d'une collection tout en l'itérant.

Heureusement, il existe une solution plus élégante que l'écriture d'une boucle for qui implique une saisie inutile et peut être source d'erreurs.

ICollection<int> test = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

foreach (int myInt in test.Reverse<int>())
{
    if (myInt % 2 == 0)
    {
        test.Remove(myInt);
    }
}

7
Cela a parfaitement fonctionné pour moi. Simple et élégant, et a nécessité des modifications minimes de mon code.
Stephen MacDougall

1
Est-ce que c'est juste un génie flippin? Et je suis d'accord avec @StephenMacDougall, je n'ai pas besoin d'utiliser ces C ++ 'y pour les boucles et de continuer avec LINQ comme prévu.
Piotr Kula

5
Je ne vois aucun avantage sur foreach simple (int myInt dans test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Vous devez encore allouer une copie pour Reverse et cela introduit Huh? moment - pourquoi est-il inversé.
jahav

11
@jedesah Oui, Reverse<T>()crée un itérateur qui parcourt la liste en arrière, mais il lui alloue un tampon supplémentaire de même taille que la liste elle-même ( referencesource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>ne se contente pas de parcourir la liste d'origine dans l'ordre inverse (sans allouer de mémoire supplémentaire). Par conséquent, les deux ToList()et Reverse()ont la même consommation de mémoire (les deux créent une copie), mais ToList()ne font rien aux données. Avec Reverse<int>(), je me demande pourquoi la liste est inversée, pour quelle raison.
jahav

1
@jahav Je vois votre point. C'est assez décevant que la mise en œuvre de Reverse<T>()crée un nouveau tampon, je ne suis pas sûr de comprendre pourquoi cela est nécessaire. Il me semble qu'en fonction de la structure sous-jacente de la Enumerable, il devrait au moins dans certains cas être possible de réaliser une itération inverse sans allouer de mémoire linéaire.
jedesah

68
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

Si vous ajoutez ".ToList ()" à votre liste (ou les résultats d'une requête LINQ), vous pouvez supprimer "item" directement de "list" sans que la redoutable " Collection ne soit modifiée; l'opération d'énumération peut ne pas s'exécuter ." Erreur. Le compilateur fait une copie de "list", afin que vous puissiez effectuer la suppression en toute sécurité sur le tableau.

Bien que ce modèle ne soit pas super efficace, il a une sensation naturelle et est suffisamment flexible pour presque toutes les situations . Par exemple, lorsque vous souhaitez enregistrer chaque "élément" dans une base de données et le supprimer de la liste uniquement lorsque la sauvegarde de la base de données a réussi.


6
C'est la meilleure solution si l'efficacité n'est pas cruciale.
IvanP

2
c'est aussi plus rapide et plus lisible: list.RemoveAll (i => true);
santiago arizti

1
@Greg Little, vous ai-je bien compris - lorsque vous ajoutez le compilateur ToList () passe par la collection copiée mais supprime de l'original?
Pyrejkee

Si votre liste contient des éléments en double et que vous souhaitez uniquement supprimer l'élément apparaissant plus tard dans la liste, cela ne supprimera-t-il pas le mauvais élément?
Tim MB

Cela fonctionne-t-il également pour une liste d'objets?
Tom el Safadi

23

Sélectionnez les éléments que vous ne voulez plutôt que d' essayer de supprimer les éléments que vous ne voulez. C'est tellement plus facile (et généralement plus efficace aussi) que de supprimer des éléments.

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);

Je voulais poster ceci en tant que commentaire en réponse au commentaire laissé par Michael Dillon ci-dessous, mais c'est trop long et probablement utile d'avoir dans ma réponse quand même:

Personnellement, je ne supprimerais jamais les éléments un par un, si vous en avez besoin, appelez alors RemoveAllqui prend un prédicat et ne réorganise le tableau interne qu'une seule fois, tandis Removequ'une Array.Copyopération est effectuée pour chaque élément que vous supprimez. RemoveAllest beaucoup plus efficace.

Et lorsque vous parcourez une liste en arrière, vous avez déjà l'index de l'élément que vous souhaitez supprimer, il serait donc beaucoup plus efficace d'appeler RemoveAt, carRemove commence par parcourir la liste pour trouver l'index de l'élément que vous essayez de supprimer, mais vous connaissez déjà cet index.

Donc, dans l'ensemble, je ne vois aucune raison d'appeler Removeune boucle for. Et idéalement, si cela est possible, utilisez le code ci-dessus pour diffuser des éléments de la liste selon les besoins afin qu'aucune seconde structure de données ne soit créée du tout.


1
Soit cela, soit ajoutez un pointeur sur les éléments indésirables dans une deuxième liste, puis une fois la boucle terminée, parcourez la liste de suppression et utilisez-la pour supprimer les éléments.
Michael Dillon

22

L'utilisation de ToArray () sur une liste générique vous permet de faire un Remove (élément) sur votre liste générique:

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }

2
Ce n'est pas faux, mais je dois souligner que cela contourne la nécessité de créer une deuxième liste de "stockage" des éléments que vous souhaitez supprimer au détriment de la copie de la liste entière dans un tableau. Une 2ème liste d'éléments triés sur le volet contiendra probablement moins d'articles.
Ahmad Mageed

20

L'utilisation de .ToList () fera une copie de votre liste, comme expliqué dans cette question: ToList () - crée-t-il une nouvelle liste?

En utilisant ToList (), vous pouvez supprimer de votre liste d'origine, car vous êtes en train d'itérer sur une copie.

foreach (var item in listTracked.ToList()) {    

        if (DetermineIfRequiresRemoval(item)) {
            listTracked.Remove(item)
        }

     }

1
Mais du point de vue des performances, vous copiez votre liste, ce qui peut prendre un certain temps. Bon et facile moyen de le faire, mais avec une performance pas si bonne
Florian K

12

Si la fonction qui détermine les éléments à supprimer n'a aucun effet secondaire et ne mute pas l'élément (c'est une fonction pure), une solution simple et efficace (temps linéaire) est:

list.RemoveAll(condition);

S'il y a des effets secondaires, j'utiliserais quelque chose comme:

var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

Il s'agit toujours d'un temps linéaire, en supposant que le hachage est bon. Mais il a une utilisation de mémoire accrue en raison du hachage.

Enfin, si votre liste est seulement un IList<T>au lieu d'un, List<T>je suggère ma réponse à Comment puis-je faire cet itérateur spécial foreach? . Cela aura un temps d'exécution linéaire étant donné les implémentations typiques de IList<T>, par rapport au temps d'exécution quadratique de nombreuses autres réponses.


11

Comme tout retrait est effectué à une condition que vous pouvez utiliser

list.RemoveAll(item => item.Value == someValue);

C'est la meilleure solution si le traitement ne mute pas l'élément et n'a aucun effet secondaire.
CodesInChaos

9
List<T> TheList = new List<T>();

TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));

8

Vous ne pouvez pas utiliser foreach, mais vous pouvez parcourir en avant et gérer votre variable d'index de boucle lorsque vous supprimez un élément, comme ceci:

for (int i = 0; i < elements.Count; i++)
{
    if (<condition>)
    {
        // Decrement the loop counter to iterate this index again, since later elements will get moved down during the remove operation.
        elements.RemoveAt(i--);
    }
}

Notez qu'en général, toutes ces techniques reposent sur le comportement de la collection en cours d'itération. La technique présentée ici fonctionnera avec la liste standard (T). (Il est tout à fait possible d'écrire votre propre classe de collection et itérateur qui ne permet le retrait de l' article lors d' une boucle foreach.)


6

Utiliser Removeou RemoveAtsur une liste tout en parcourant cette liste a été intentionnellement rendu difficile, car ce n'est presque toujours pas la bonne chose à faire . Vous pourriez peut-être le faire fonctionner avec une astuce intelligente, mais ce serait extrêmement lent. Chaque fois que vous appelez, Removeil doit parcourir toute la liste pour trouver l'élément que vous souhaitez supprimer. Chaque fois que vous appelez, RemoveAtil doit déplacer les éléments suivants d'une position vers la gauche. Ainsi, toute solution utilisant Removeou RemoveAt, nécessiterait un temps quadratique, O (n²) .

Utilisez RemoveAllsi vous le pouvez. Sinon, le modèle suivant filtrera la liste en place en temps linéaire, O (n) .

// Create a list to be filtered
IList<int> elements = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
// Filter the list
int kept = 0;
for (int i = 0; i < elements.Count; i++) {
    // Test whether this is an element that we want to keep.
    if (elements[i] % 3 > 0) {
        // Add it to the list of kept elements.
        elements[kept] = elements[i];
        kept++;
    }
}
// Unfortunately IList has no Resize method. So instead we
// remove the last element of the list until: elements.Count == kept.
while (kept < elements.Count) elements.RemoveAt(elements.Count-1);

3

Je souhaite que le "modèle" soit quelque chose comme ceci:

foreach( thing in thingpile )
{
    if( /* condition#1 */ )
    {
        foreach.markfordeleting( thing );
    }
    elseif( /* condition#2 */ )
    {
        foreach.markforkeeping( thing );
    }
} 
foreachcompleted
{
    // then the programmer's choices would be:

    // delete everything that was marked for deleting
    foreach.deletenow(thingpile); 

    // ...or... keep only things that were marked for keeping
    foreach.keepnow(thingpile);

    // ...or even... make a new list of the unmarked items
    others = foreach.unmarked(thingpile);   
}

Cela alignerait le code sur le processus qui se déroule dans le cerveau du programmeur.


1
Assez facile. Il suffit de créer un tableau d'indicateurs booléens (utilisez un type à 3 états, par exemple Nullable<bool>, si vous souhaitez autoriser le non marqué), puis utilisez-le après foreach pour supprimer / conserver les éléments.
Dan Bechard

3

En supposant que le prédicat est une propriété booléenne d'un élément, que s'il est vrai, l'élément doit être supprimé:

        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }

J'ai donné une note positive à cela, car il pourrait parfois être plus efficace de parcourir la liste dans l'ordre (et non dans l'ordre inverse). Vous pourriez peut-être vous arrêter lors de la recherche du premier élément à ne pas supprimer car la liste est ordonnée. (Imaginez une «pause» où se trouve l'i ++ dans cet exemple.
FrankKrumnow

3

Je voudrais réaffecter la liste à partir d'une requête LINQ qui a filtré les éléments que vous ne vouliez pas conserver.

list = list.Where(item => ...).ToList();

À moins que la liste ne soit très longue, il ne devrait y avoir aucun problème de performances significatif.


3

La meilleure façon de supprimer des éléments d'une liste tout en itérant dessus est d'utiliser RemoveAll() . Mais la principale préoccupation écrite par les gens est qu'ils doivent faire des choses complexes à l'intérieur de la boucle et / ou avoir des cas de comparaison complexes.

La solution est de toujours utiliser RemoveAll()mais utilisez cette notation:

var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(item => 
{
    // Do some complex operations here
    // Or even some operations on the items
    SomeFunction(item);
    // In the end return true if the item is to be removed. False otherwise
    return item > 5;
});

2
foreach(var item in list.ToList())

{

if(item.Delete) list.Remove(item);

}

Créez simplement une liste entièrement nouvelle à partir de la première. Je dis "Facile" plutôt que "Droite" car la création d'une liste entièrement nouvelle vient probablement à une performance supérieure à la méthode précédente (je n'ai pas pris la peine de faire un benchmarking.) Je préfère généralement ce modèle, il peut également être utile pour surmonter Limitations de Linq-To-Entities.

for(i = list.Count()-1;i>=0;i--)

{

item=list[i];

if (item.Delete) list.Remove(item);

}

De cette façon, vous parcourez la liste en arrière avec une ancienne boucle For. Faire cela en avant pourrait être problématique si la taille de la collection change, mais en arrière devrait toujours être sûr.


1

Copiez la liste que vous parcourez. Retirez ensuite de la copie et intérez l'original. Revenir en arrière est déroutant et ne fonctionne pas bien lors d'une boucle en parallèle.

var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});

1

Je voudrais faire ça

using System.IO;
using System;
using System.Collections.Generic;

class Author
    {
        public string Firstname;
        public string Lastname;
        public int no;
    }

class Program
{
    private static bool isEven(int i) 
    { 
        return ((i % 2) == 0); 
    } 

    static void Main()
    {    
        var authorsList = new List<Author>()
        {
            new Author{ Firstname = "Bob", Lastname = "Smith", no = 2 },
            new Author{ Firstname = "Fred", Lastname = "Jones", no = 3 },
            new Author{ Firstname = "Brian", Lastname = "Brains", no = 4 },
            new Author{ Firstname = "Billy", Lastname = "TheKid", no = 1 }
        };

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        {
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        }
    }
}

PRODUCTION

Fred Jones
Billy TheKid

0

Je me suis retrouvé dans une situation similaire où je devais supprimer chaque nième élément dans une donnée List<T>.

for (int i = 0, j = 0, n = 3; i < list.Count; i++)
{
    if ((j + 1) % n == 0) //Check current iteration is at the nth interval
    {
        list.RemoveAt(i);
        j++; //This extra addition is necessary. Without it j will wrap
             //down to zero, which will throw off our index.
    }
    j++; //This will always advance the j counter
}

0

Le coût de la suppression d'un élément de la liste est proportionnel au nombre d'éléments suivant celui à supprimer. Dans le cas où la première moitié des éléments peut être supprimée, toute approche basée sur la suppression individuelle des éléments finira par devoir effectuer environ N * N / 4 opérations de copie d'élément, ce qui peut devenir très coûteux si la liste est longue. .

Une approche plus rapide consiste à parcourir la liste pour trouver le premier élément à supprimer (le cas échéant), puis à partir de ce moment, copier chaque élément qui doit être conservé à l'endroit où il appartient. Une fois cela fait, si les éléments R doivent être conservés, les premiers éléments R de la liste seront ces éléments R, et tous les éléments nécessitant une suppression seront à la fin. Si ces éléments sont supprimés dans l'ordre inverse, le système n'aura pas à en copier aucun, donc si la liste avait N éléments dont R éléments, y compris tous les premiers F, ont été conservés, il sera nécessaire de copiez les éléments RF et réduisez la liste d'un élément NR fois. Tout le temps linéaire.


0

Mon approche est que je crée d'abord une liste d'indices, qui devraient être supprimés. Ensuite, je passe en revue les index et supprime les éléments de la liste initiale. Cela ressemble à ceci:

var messageList = ...;
// Restrict your list to certain criteria
var customMessageList = messageList.FindAll(m => m.UserId == someId);

if (customMessageList != null && customMessageList.Count > 0)
{
    // Create list with positions in origin list
    List<int> positionList = new List<int>();
    foreach (var message in customMessageList)
    {
        var position = messageList.FindIndex(m => m.MessageId == message.MessageId);
        if (position != -1)
            positionList.Add(position);
    }
    // To be able to remove the items in the origin list, we do it backwards
    // so that the order of indices stays the same
    positionList = positionList.OrderByDescending(p => p).ToList();
    foreach (var position in positionList)
    {
        messageList.RemoveAt(position);
    }
}

0

En C #, un moyen simple consiste à marquer ceux que vous souhaitez supprimer puis à créer une nouvelle liste à parcourir ...

foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}  

ou encore plus simple utiliser linq ....

list.RemoveAll(p=>p.Delete);

mais il vaut la peine de se demander si d'autres tâches ou threads auront accès à la même liste en même temps que vous êtes occupé à les supprimer, et peut-être utiliser une ConcurrentList à la place.


0

Tracez les éléments à supprimer avec une propriété et supprimez-les tous après le processus.

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)
{
    if (cond) 
    {
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    }
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);

0

Je voulais juste ajouter mes 2 cents à cela au cas où cela aiderait quelqu'un, j'avais un problème similaire, mais je devais supprimer plusieurs éléments d'une liste de tableaux pendant qu'elle était itérée. la réponse la plus élevée a fait le plus pour moi jusqu'à ce que je rencontre des erreurs et que je réalise que l'index était supérieur à la taille de la liste de tableaux dans certains cas parce que plusieurs éléments étaient supprimés mais que l'index de la boucle ne tenait pas suivre cela. J'ai corrigé cela avec une simple vérification:

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--){
    if(i>= place_holder.Count){
        i = place_holder.Count-1; 
    }

// some method that removes multiple elements here
}

-4
myList.RemoveAt(i--);

simples;

1
Que fait simples;-on ici?
Denny

6
c'est un délégué .. il downvotes ma réponse à chaque fois qu'il s'exécute
SW

SW ROFL! Je dois aimer ton commentaire.
Erick Brown
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.