web-dev-qa-db-fra.com

Quel est le meilleur moyen de modifier une liste dans une boucle 'foreach'?

Une nouvelle fonctionnalité de C #/.NET 4.0 est que vous pouvez modifier votre énumération dans une variable foreach sans obtenir l'exception. Voir l'entrée de blog de Paul Jackson Un effet secondaire intéressant de la simultanéité: supprimer des éléments d'une collection en énumérant pour plus d'informations sur ce changement.

Quelle est la meilleure façon de faire ce qui suit?

foreach(var item in Enumerable)
{
    foreach(var item2 in item.Enumerable)
    {
        item.Add(new item2)
    }
}

Habituellement, j'utilise une IList comme cache/tampon jusqu'à la fin de la foreach, mais existe-t-il un meilleur moyen?

60
Polo

La collection utilisée dans foreach est immuable. C'est très bien par conception. 

Comme il est dit sur MSDN :

L'instruction foreach est utilisée pour parcourez la collection pour obtenir l'information que vous voulez, mais que vous pouvez ne pas être utilisé pour ajouter ou supprimer des éléments de la collection source à éviter effets secondaires imprévisibles. Si vous besoin d'ajouter ou de supprimer des éléments du fichier collection source, utilisez une boucle for.

La publication dans le link fournie par Poko indique que cela est autorisé dans les nouvelles collections simultanées. 

65
Rik

Faites une copie de l'énumération, en utilisant une méthode d'extension IEnumerable dans ce cas, et énumérez-la par-dessus. Cela ajouterait une copie de chaque élément de chaque énumérable interne à cette énumération.

foreach(var item in Enumerable)
{
    foreach(var item2 in item.Enumerable.ToList())
    {
        item.Add(item2)
    }
}
14
tvanfosson

Comme mentionné, mais avec un exemple de code:

foreach(var item in collection.ToArray())
    collection.Add(new Item...);
6
eulerfx

Pour illustrer la réponse de Nippysaurus: Si vous allez sur ajouter les nouveaux éléments à la liste et que vous souhaitez également traiter les éléments récemment ajoutés au cours de la même énumération, vous pouvez simplement utiliser pour boucle au lieu de pour chaque boucle, problème résolu :)

var list = new List<YourData>();
... populate the list ...

//foreach (var entryToProcess in list)
for (int i = 0; i < list.Count; i++)
{
    var entryToProcess = list[i];

    var resultOfProcessing = DoStuffToEntry(entryToProcess);

    if (... condition ...)
        list.Add(new YourData(...));
}

Par exemple, exécutable:

void Main()
{
    var list = new List<int>();
    for (int i = 0; i < 10; i++)
        list.Add(i);

    //foreach (var entry in list)
    for (int i = 0; i < list.Count; i++)
    {
        var entry = list[i];
        if (entry % 2 == 0)
            list.Add(entry + 1);

        Console.Write(entry + ", ");
    }

    Console.Write(list);
}

Sortie du dernier exemple:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 3, 5, 7, 9, 

Liste (15 éléments)
0
1
2
3
4
5
6
7
8
9
1
3
5
7

6
Roland Pihlakas

Voici comment vous pouvez le faire (solution rapide et compliquée. Si vous vraiment avez besoin de ce genre de comportement, vous devez soit revoir votre conception, soit remplacer tous les membres IList<T> et agréger la liste des sources):

using System;
using System.Collections.Generic;

namespace ConsoleApplication3
{
    public class ModifiableList<T> : List<T>
    {
        private readonly IList<T> pendingAdditions = new List<T>();
        private int activeEnumerators = 0;

        public ModifiableList(IEnumerable<T> collection) : base(collection)
        {
        }

        public ModifiableList()
        {
        }

        public new void Add(T t)
        {
            if(activeEnumerators == 0)
                base.Add(t);
            else
                pendingAdditions.Add(t);
        }

        public new IEnumerator<T> GetEnumerator()
        {
            ++activeEnumerators;

            foreach(T t in ((IList<T>)this))
                yield return t;

            --activeEnumerators;

            AddRange(pendingAdditions);
            pendingAdditions.Clear();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ModifiableList<int> ints = new ModifiableList<int>(new int[] { 2, 4, 6, 8 });

            foreach(int i in ints)
                ints.Add(i * 2);

            foreach(int i in ints)
                Console.WriteLine(i * 2);
        }
    }
}
4
Anton Gogolev

LINQ est très efficace pour jongler avec les collections.

Vos types et votre structure ne sont pas clairs pour moi, mais je vais essayer d’adapter votre exemple au mieux de mes capacités.

D'après votre code, il apparaît que, pour chaque élément, vous ajoutez à celui-ci tout ce qui provient de sa propre propriété 'Enumerable'. C'est très simple:

foreach (var item in Enumerable)
{
    item = item.AddRange(item.Enumerable));
}

Comme exemple plus général, supposons que nous voulions itérer une collection et supprimer des éléments pour lesquels une condition donnée est vraie. Éviter foreach, en utilisant LINQ:

myCollection = myCollection.Where(item => item.ShouldBeKept);

Ajouter un élément basé sur chaque élément existant? Aucun problème:

myCollection = myCollection.Concat(myCollection.Select(item => new Item(item.SomeProp)));
2
Timo

Vous ne pouvez pas modifier la collection énumérable pendant son énumération. Vous devrez donc apporter vos modifications avant ou après l'énumération.

La boucle for est une alternative intéressante, mais si votre collection IEnumerable n'implémente pas ICollection, cela n'est pas possible.

Non plus: 

1) Copiez la collection en premier. Énumérer la collection copiée et modifier la collection d'origine pendant l'énumération. (@tvanfosson)

ou

2) Conservez une liste des modifications et validez-les après l'énumération.

1
Josh G

La meilleure approche en termes de performances consiste probablement à utiliser un ou deux tableaux. Copiez la liste dans un tableau, effectuez des opérations sur le tableau, puis créez une nouvelle liste à partir du tableau. L'accès à un élément de tableau est plus rapide que l'accès à un élément de liste et les conversions entre un List<T> et un T[] peuvent utiliser une opération de "copie en bloc" rapide qui évite la surcharge associée à l'accès à des éléments individuels.

Par exemple, supposons que vous avez un List<string> et souhaitez que chaque chaîne de la liste qui commence par T soit suivie d'un élément "Boo", tandis que chaque chaîne commençant par "U" est entièrement supprimée. Une approche optimale serait probablement quelque chose comme:

int srcPtr,destPtr;
string[] arr;

srcPtr = theList.Count;
arr = new string[srcPtr*2];
theList.CopyTo(arr, theList.Count); // Copy into second half of the array
destPtr = 0;
for (; srcPtr < arr.Length; srcPtr++)
{
  string st = arr[srcPtr];
  char ch = (st ?? "!")[0]; // Get first character of string, or "!" if empty
  if (ch != 'U')
    arr[destPtr++] = st;
  if (ch == 'T')
    arr[destPtr++] = "Boo";
}
if (destPtr > arr.Length/2) // More than half of dest. array is used
{
  theList = new List<String>(arr); // Adds extra elements
  if (destPtr != arr.Length)
    theList.RemoveRange(destPtr, arr.Length-destPtr); // Chop to proper length
}
else
{
  Array.Resize(ref arr, destPtr);
  theList = new List<String>(arr); // Adds extra elements
}

Il aurait été utile que List<T> fournisse une méthode pour construire une liste à partir d'une partie d'un tableau, mais je ne connais aucune méthode efficace pour le faire. Pourtant, les opérations sur les tableaux sont assez rapides. Il est à noter que l'ajout et la suppression d'éléments de la liste n'exigent pas de "pousser" d'autres objets; chaque élément est écrit directement à son emplacement approprié dans le tableau.

1
supercat

Pour ajouter à la réponse de Timo, LINQ peut également être utilisé comme ceci:

items = items.Select(i => {

     ...
     //perform some logic adding / updating.

     return i / return new Item();
     ...

     //To remove an item simply have logic to return null.

     //Then attach the Where to filter out nulls

     return null;
     ...


}).Where(i => i != null);
0
DDiVita

Vous devriez vraiment utiliser for() au lieu de foreach() dans ce cas.

0
Nippysaurus

J'ai écrit une étape facile, mais à cause de cette performance sera dégradé

Voici mon extrait de code: -

for (int tempReg = 0; tempReg < reg.Matches(lines).Count; tempReg++)
                            {
                                foreach (Match match in reg.Matches(lines))
                                {
                                    var aStringBuilder = new StringBuilder(lines);
                                    aStringBuilder.Insert(startIndex, match.ToString().Replace(",", " ");
                                    lines[k] = aStringBuilder.ToString();
                                    tempReg = 0;
                                    break;
                                }
                            }
0
pravin ghare