web-dev-qa-db-fra.com

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

Je cherche un meilleur modèle pour travailler avec une liste d'éléments que chacun doit traiter, puis en fonction du résultat, sont supprimés de la liste.

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

Y a-t-il une manière élégante de faire ceci?

405

Itérez 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 à tester:

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));
663
Ahmad Mageed

Une solution simple et directe:

Utilisez un standard pour la boucle en cours d'exécution à l'envers sur votre collection et RemoveAt(i) pour supprimer des éléments.

83
Jan

L'itération inversée doit être la première chose qui vous vient à l'esprit lorsque vous souhaitez supprimer des éléments d'une collection en effectuant une itération sur celle-ci.

Heureusement, il existe une solution plus élégante que l'écriture d'une boucle for qui implique une frappe inutile et peut être sujette à erreur.

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);
    }
}
64
jedesah
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

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

Bien que ce modèle ne soit pas super efficace, il a un toucher naturel 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 aboutit.

53
Greg Little

L'utilisation de ToArray () sur une liste générique vous permet d'effectuer une suppression (é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);
        }
21
Etienne Brouillard

Sélectionnez les éléments que vous voulez plutôt que d'essayer de supprimer les éléments que vous ne voulez pas . 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 ma réponse de toute façon:

Personnellement, je ne supprimerais jamais les éléments un par un; si vous en avez besoin, appelez RemoveAll qui prend un prédicat et ne réorganise le tableau interne qu'une seule fois, alors que Remove effectue un Array.Copy opération pour chaque élément supprimé. RemoveAll est 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 bien plus efficace d'appeler RemoveAt, car Remove effectue d'abord un parcours de 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 de jamais appeler Remove dans une boucle for. Et idéalement, si cela est possible, utilisez le code ci-dessus pour diffuser les éléments de la liste selon vos besoins, de sorte qu'aucune deuxième structure de données ne soit créée.

20
JulianR

Utiliser .ToList () créera 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 effectuez une itération sur une copie.

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

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

     }
18
StuartQ

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

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

C'est toujours le temps linéaire, en supposant que le hachage est bon. Mais l'utilisation de la mémoire est plus importante en raison du hashset.

Enfin, si votre liste n’est qu’un IList<T> au lieu d’un List<T>, je suggère ma réponse à Comment puis-je faire cette spéciale pour chaque itérateur? . 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.

12
CodesInChaos

Comme toute suppression est prise à une condition que vous pouvez utiliser

list.RemoveAll(item => item.Value == someValue);
10
Ahmad
List<T> TheList = new List<T>();

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

Vous ne pouvez pas utiliser foreach, mais vous pouvez effectuer des itérations et gérer votre variable d'index de boucle lorsque vous supprimez un élément, comme suit:

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 itérée. 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 votre propre itérateur qui ne le fait autorise la suppression d'éléments lors d'une boucle foreach.)

7
yoyo

Utiliser Remove ou RemoveAt sur une liste tout en itérant sur cette liste a été délibérément rendu difficile, car elle est presque toujours la mauvaise chose à faire. Vous pourriez peut-être le faire fonctionner avec une astuce intelligente, mais ce serait extrêmement lent. Chaque fois que vous appelez Remove, il doit parcourir toute la liste pour trouver l'élément que vous souhaitez supprimer. Chaque fois que vous appelez RemoveAt, il doit déplacer les éléments suivants d'une position vers la gauche. En tant que telle, toute solution utilisant Remove ou RemoveAt, nécessiterait un temps quadratique, O (n²) .

Utilisez RemoveAll si vous le pouvez. Sinon, le modèle suivant filtrera la liste sur place de manière linéaire. temps, 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);
5
bcmpinc

Le meilleur moyen de supprimer des éléments d’une liste en effectuant une itération est d’utiliser RemoveAll(). Mais la principale préoccupation exprimée par les gens est qu’ils doivent faire des choses complexes dans la boucle et/ou avoir des cas de comparaison complexes.

La solution est de continuer à 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;
});
3
Hüseyin Yağlı

Je souhaite le "motif" ressemblait à 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 en cours dans le cerveau du programmeur.

3
warrens

Je réaffecterais la liste à partir d'une requête LINQ qui filtrerait les éléments que vous ne souhaitiez pas conserver.

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

À moins que la liste ne soit très longue, cela ne devrait poser aucun problème de performances important.

3
Martin Liversage
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 "correct", car la création d'une liste entièrement nouvelle entraîne probablement une prime de performance par rapport à la méthode précédente (je ne me suis pas soucié de l'analyse comparative). 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 avec une ancienne boucle For. Faire ce qui précède peut être problématique si la taille de la collection change, mais inversement devrait toujours être sûr.

2
Lijo

En supposant que prédicat est une propriété booléenne d'un élément, si elle est vraie, l'élément doit alors être supprimé:

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

En C #, un moyen simple est de marquer ceux que vous souhaitez supprimer, puis de 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 est utile de déterminer si d'autres tâches ou threads auront accès à la même liste en même temps que vous êtes en train de supprimer, et utilisez peut-être plutôt une liste ConcurrentList.

0
andrew pate

Je me suis retrouvé dans une situation similaire où je devais enlever tous les nth élément dans un List<T> donné.

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
Paul Nelson Baker

Mon approche est que je crée d'abord une liste d'index, qui devrait être supprimée. Ensuite, je boucle sur 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
testing

Le coût de suppression d'un élément de la liste est proportionnel au nombre d'éléments qui suivent celui à supprimer. Dans le cas où la première moitié des éléments peut être supprimée, toute approche basée sur la suppression d'éléments individuellement finira par devoir effectuer environ N * N/4 opérations de copie d'éléments, ce qui peut coûter très cher 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 point, copier chaque élément à conserver à l'emplacement auquel 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 à supprimer seront à la fin. Si ces éléments sont supprimés dans l'ordre inverse, le système ne finira par en copier aucun. Par conséquent, si la liste contenait N éléments dont R éléments, y compris tous les premiers F, ont été conservés, il sera nécessaire de copier des éléments RF et réduire la liste d’un élément NR fois. Tout le temps linéaire.

0
supercat

Copiez la liste que vous parcourez. Retirez ensuite de la copie et interposez l'original. Revenir en arrière est source de confusion et ne fonctionne pas bien quand on boucle en parallèle.

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

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