Comment supprimer des éléments d'une liste générique tout en itérant dessus?
je cherche un meilleur " pattern pour travailler avec une liste d'éléments qui doivent être traités et puis en fonction du résultat sont retirés de la liste.
vous ne pouvez pas utiliser .Remove(element)
à l'intérieur d'un foreach (var element in X)
(parce qu'il résulte en 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 relative à i
.
y a-t-il une façon élégante de faire cela?
22 réponses
itérez votre liste à l'envers avec une boucle pour:
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 RemoveAll method avec un prédicat pour tester contre:
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));
une solution simple et directe:
utilisez une boucle standard pour exécuter en arrière sur votre collection et RemoveAt(i)
pour supprimer des éléments.
l'itération inversée devrait être la première chose à venir à l'esprit quand vous voulez supprimer des éléments d'une Collection tout en itérant sur elle.
heureusement, il y a une solution plus élégante que l'écriture d'une boucle for qui implique la dactylographie inutile et peut être sujet à des 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);
}
}
foreach (var item in list.ToList()) {
list.Remove(item);
}
Si vous ajouter ".ToList ()" à votre liste (ou les résultats D'une requête LINQ), vous pouvez supprimer "item" directement de " liste "sans la redoutée" Collection a été modifié; opération d'énumération ne peut pas exécuter ." erreur. Le compilateur fait une copie de" list", de sorte que vous pouvez faire en toute sécurité la suppression sur le tableau.
alors que Ce modèle n'est pas super efficace, il a une sensation naturelle et est assez flexible pour presque n'importe quelle situation . Comme quand vous voulez sauver chaque "élément" à une DB et le retirer de la liste que lorsque la sauvegarde de DB réussit.
L'utilisation de The ToArray () sur une liste générique vous permet de faire un Remove(item) 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);
}
Sélectionnez les éléments que vous faire voulez plutôt que d'essayer de supprimer les éléments que vous ne pas le souhaitez. C'est tellement plus facile (et généralement plus efficace aussi) que la suppression des éléments.
var newSequence = (from el in list
where el.Something || el.AnotherThing < 0
select el);
j'ai voulu poster ceci comme un commentaire en réponse au commentaire laissé par Michael Dillon ci-dessous, mais il est trop long et probablement utile d'avoir dans ma réponse de toute façon:
personnellement, je n'aurais jamais retirez les éléments un à un, si vous avez besoin de suppression, puis appelez RemoveAll
qui prend un prédicat et réarrange seulement le tableau interne une fois, tandis que Remove
fait une opération Array.Copy
pour chaque élément que vous supprimez. RemoveAll
est beaucoup plus efficace.
et quand vous êtes en arrière itérant sur une liste, vous avez déjà l'index de l'élément que vous voulez supprimer, il serait donc beaucoup plus efficace d'appeler RemoveAt
, parce que Remove
fait d'abord un traversée de la liste pour trouver l'index de l'élément que vous essayez de supprimer, mais vous connaissez déjà cet index.
donc, en somme, Je ne vois aucune raison d'appeler Remove
en boucle. Et idéalement, si c'est possible, utilisez le code ci-dessus pour diffuser les éléments de la liste au besoin, de sorte qu'aucune seconde structure de données ne doit être créée.
à l'Aide .ToList() faites 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 originale, parce que vous êtes en train d'itérer une copie.
foreach (var item in listTracked.ToList()) {
if (DetermineIfRequiresRemoval(item)) {
listTracked.Remove(item)
}
}
comme tout supprimer est pris à une condition que vous pouvez utiliser""
list.RemoveAll(item => item.Value == someValue);
si la fonction qui détermine quels articles supprimer n'a pas d'effets secondaires et ne mute pas l'article (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);
C'est encore le temps linéaire, en supposant que le hachage est bon. Mais il a une utilisation de mémoire accrue en raison du hashset.
enfin si votre liste est seulement IList<T>
au lieu d'un List<T>
je suggère ma réponse à Comment puis-je faire ce pourchaque itérateur spécial? . Ceci aura une exécution linéaire étant donné les implémentations typiques de IList<T>
, par rapport à l'exécution quadratique de beaucoup d'autres réponses.
List<T> TheList = new List<T>();
TheList.FindAll(element => element.Satisfies(Condition)).ForEach(element => TheList.Remove(element));
vous ne pouvez pas utiliser foreach, mais vous pouvez itérer en avant et gérer votre variable d'index de boucle lorsque vous supprimez un élément, comme cela:
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 iterator que ne permettre l'enlèvement d'élément pendant une boucle foreach.)
utiliser Remove
ou RemoveAt
sur une liste tout en itérant sur cette liste a été intentionnellement rendu difficile, parce qu'il est presque toujours la mauvaise chose à faire . Vous pourriez être en mesure de le faire fonctionner avec certains astuce, mais c'est extrêmement lent. Chaque fois que vous appelez Remove
il a à parcourir toute la liste pour trouver l'élément que vous voulez supprimer. Chaque fois que vous appelez RemoveAt
il doit déplacer les éléments suivants 1 position à gauche. Ainsi , toute solution utilisant Remove
ou RemoveAt
exigerait un temps quadratique, O(n2) .
utilisez RemoveAll
si vous le pouvez. Autrement, le pattern 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);
je souhait le "modèle" était 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 avec le processus qui se déroule dans le cerveau du programmeur.
en présumant que prédicat est une propriété booléenne d'un élément, que si elle est vraie, alors l'élément devrait être supprimé:
int i = 0;
while (i < list.Count())
{
if (list[i].predicate == true)
{
list.RemoveAt(i);
continue;
}
i++;
}
Je réassignais la liste à partir d'une requête LINQ qui filtrait les éléments que vous ne vouliez pas garder.
list = list.Where(item => ...).ToList();
à moins que la liste ne soit très longue, il ne devrait pas y avoir de problèmes de rendement importants.
la meilleure façon de supprimer des éléments d'une liste tout en itérant sur elle est d'utiliser RemoveAll()
. Mais la principale préoccupation écrite par les gens est qu'ils doivent faire certaines choses complexes à l'intérieur de la boucle et/ou avoir des cas comparables complexes.
la solution est d'utiliser encore RemoveAll()
mais utiliser 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;
});
foreach(var item in list.ToList())
{
if(item.Delete) list.Remove(item);
}
crée simplement une liste entièrement nouvelle à partir de la première. Je dis" facile "plutôt que" juste " car la création d'une liste entièrement nouvelle vient probablement à une prime de performance par rapport à la méthode précédente (je n'ai pas pris la peine avec n'importe quel benchmarking.) Je préfère généralement Ce modèle, il peut également être utile pour surmonter les limites de Linq-to-Entities.
for(i = list.Count()-1;i>=0;i--)
{
item=list[i];
if (item.Delete) list.Remove(item);
}
cette façon tourne à l'envers à travers la liste avec un simple vieux pour boucle. Ce faisant attaquants puissent être problématique si la taille de la collection des changements, mais à l'envers devrait toujours être en sécurité.
je me suis trouvé dans une situation similaire où j'ai dû enlever chaque élément n th 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
}
Le coût de la suppression d'un élément de la liste est proportionnelle au nombre d'articles suivant celui à supprimer. Dans le cas où la première moitié des articles se qualifient pour le retrait, n'importe quelle approche qui est basée sur le retrait des articles individuellement finira par avoir à effectuer environ N*N/4 des opérations de copie d'article, qui peuvent devenir très coûteux si la liste est grande.
Une rapide approche consiste à parcourir la liste pour trouver le premier élément à supprimer (le cas échéant), et puis, à partir de ce moment, copie de chaque élément qui doit être conservé à l'endroit où il appartient. Une fois que cela est fait, si les éléments R doivent être conservés, les premiers éléments R de la liste seront les éléments R, et tous les éléments devant être supprimés seront à la fin. Si ces éléments sont supprimés dans l'ordre inverse, le système n'aura plus à copier aucun d'entre eux, donc si la liste avait N éléments dont R, y compris tous les premiers F, ont été conservés, il sera nécessaire de copier les éléments R-f, et réduire la liste D'un point N-R fois. Tous les temps linéaire.
Mon approche est que j'ai d'abord créer une liste d'indices qui devraient être supprimés. Après, je boucle sur les indices et supprimer 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);
}
}
copiez la liste que vous itérez. Puis retirez de la copie et entrez l'original. Faire marche arrière est déroutant et ne fonctionne pas bien en boucle en parallèle.
var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();
Parallel.ForEach(iterableIds, id =>
{
ids.Remove(id);
});