Was ist der beste Weg, um eine Liste in einer 'foreach'-Schleife zu ändern?


77

Eine neue Funktion in C # / .NET 4.0 ist, dass Sie Ihre Aufzählung in a ändern können, foreachohne die Ausnahme zu erhalten. Weitere Informationen zu dieser Änderung finden Sie in Paul Jacksons Blogeintrag Ein interessanter Nebeneffekt der Parallelität: Entfernen von Elementen aus einer Sammlung während der Aufzählung .

Was ist der beste Weg, um Folgendes zu tun?

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

Normalerweise benutze ich einen IListals Cache / Puffer bis zum Ende des foreach, aber gibt es einen besseren Weg?


2
Hmm .. Können Sie uns auf die Dokumentation dieser Änderung verweisen? Aufzählungen waren bei der Aufzählung über die Sammlung immer unveränderlich.
CraigTP

Dies ist eine Variation des Themas, die Steve McConnell dazu veranlasste, niemals mit dem Loop-Index zu affen .
Ed Guiness

4
Ich weiß, dass ich hier ein sehr altes Gespräch aufbaue, aber ich würde äußerst vorsichtig damit umgehen. Nur die neuen gleichzeitigen Sammlungen können innerhalb von foreach geändert werden - alle vorherigen Sammlungstypen, und ich stelle mir vor, dass die meisten zukünftigen Sammlungstypen auch unveränderlich bleiben, wenn sie über sie aufgezählt werden. Wenn Sie diese Eigenart ausgiebig nutzen, können Sie die gleichzeitigen Sammlungen effektiv verwenden. Wenn Sie in Zukunft eine andere Sammlung verwenden möchten, werden plötzlich alle Ihre eigenartigen foreach-Schleifen unterbrochen.
Chris


Stellt sich diese Frage speziell auf die gleichzeitigen Sammlungen? Oder stellt es eine allgemeinere Frage und erwähnt dies nur als Kontrast?
UuDdLrLrSs

Antworten:


78

Die in foreach verwendete Sammlung ist unveränderlich. Dies ist sehr beabsichtigt.

Wie es auf MSDN heißt :

Die foreach-Anweisung wird verwendet, um die Sammlung zu durchlaufen, um die gewünschten Informationen abzurufen. Sie kann jedoch nicht zum Hinzufügen oder Entfernen von Elementen zur Quellensammlung verwendet werden, um unvorhersehbare Nebenwirkungen zu vermeiden. Wenn Sie Elemente zur Quellensammlung hinzufügen oder daraus entfernen müssen, verwenden Sie eine for-Schleife.

Der Beitrag in dem von Poko bereitgestellten Link zeigt an, dass dies in den neuen gleichzeitigen Sammlungen zulässig ist.


21
Ihre Antwort beantwortet die Frage nicht wirklich. Er weiß, dass die grundlegende foreach-Schleife unveränderlich ist ... er möchte wissen, wie Änderungen am besten auf eine aufzählbare Sammlung angewendet werden können.
Josh G

13
Dann lautet die Antwort: Verwenden Sie eine reguläre for-Schleife, wie im Zitat vorgeschlagen. Mir ist bekannt, dass das OP diese Verhaltensänderungen in C # 4.0 erwähnt, aber ich kann keine Informationen dazu finden. Im Moment denke ich, dass dies noch eine relevante Antwort ist.
Rik

14

Erstellen Sie eine Kopie der Aufzählung mit einer IEnumerable-Erweiterungsmethode in diesem Fall und führen Sie eine Aufzählung darüber auf. Dies würde eine Kopie jedes Elements in jeder inneren Aufzählung zu dieser Aufzählung hinzufügen.

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

1
Aber warum sollte man die Aufzählung in eine Liste anstatt in ein Array kopieren? Eine Liste enthält Methoden zum Suchen, Sortieren und Bearbeiten der Sammlung, die wir nicht benötigen.
Rudey

7

Wie erwähnt, aber mit einem Codebeispiel:

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

Dies ist keine gute Antwort, da unnötige Zuweisungen und Kopien ( ToArray()) ausgeführt werden, um die Liste zu durchlaufen.
Antiduh

7

Um die Antwort von Nippysaurus zu veranschaulichen: Wenn Sie die neuen Elemente zur Liste hinzufügen und auch die neu hinzugefügten Elemente während derselben Aufzählung verarbeiten möchten, können Sie nur for- Schleife anstelle von foreach- Schleife verwenden, Problem gelöst :)

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(...));
}

Für ein lauffähiges Beispiel:

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

Ausgabe des letzten Beispiels:

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

Liste (15 Elemente)
0
1
2
3
4
5
6
7
8
9
1
3
5
7
9


3

Sie können die Aufzählungssammlung während der Aufzählung nicht ändern, daher müssen Sie Ihre Änderungen vor oder nach der Aufzählung vornehmen.

Die forSchleife ist eine gute Alternative, aber wenn Ihre IEnumerableSammlung nicht implementiert wird ICollection, ist dies nicht möglich.

Entweder:

1) Kopieren Sie zuerst die Sammlung. Zählen Sie die kopierte Sammlung auf und ändern Sie die ursprüngliche Sammlung während der Aufzählung. (@tvanfosson)

oder

2) Führen Sie eine Liste der Änderungen und übernehmen Sie diese nach der Aufzählung.


3

So können Sie das tun (schnelle und schmutzige Lösung. Wenn Sie diese Art von Verhalten wirklich benötigen, sollten Sie entweder Ihr Design überdenken oder alle IList<T>Mitglieder überschreiben und die Quellliste aggregieren):

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

3

LINQ ist sehr effektiv zum Jonglieren mit Sammlungen.

Ihre Typen und Strukturen sind mir unklar, aber ich werde versuchen, Ihr Beispiel nach besten Kräften zu gestalten.

Aus Ihrem Code geht hervor, dass Sie für jedes Element alles aus seiner eigenen Eigenschaft 'Aufzählbar' zu diesem Element hinzufügen. Das ist sehr einfach:

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

Nehmen wir als allgemeineres Beispiel an, wir möchten eine Sammlung iterieren und Elemente entfernen, bei denen eine bestimmte Bedingung erfüllt ist. Vermeiden foreach, mit LINQ:

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

Einen Artikel basierend auf jedem vorhandenen Artikel hinzufügen? Kein Problem:

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

1

Aus Sicht der Leistung ist es wahrscheinlich am besten, ein oder zwei Arrays zu verwenden. Kopieren Sie die Liste in ein Array, führen Sie Operationen am Array aus und erstellen Sie dann eine neue Liste aus dem Array. Der Zugriff auf ein Array-Element ist schneller als der Zugriff auf ein Listenelement. Bei Konvertierungen zwischen a List<T>und a T[]kann eine schnelle "Massenkopie" -Operation verwendet werden, die den mit dem Zugriff auf einzelne Elemente verbundenen Overhead vermeidet.

Angenommen, Sie haben eine List<string>und möchten, dass jeder Zeichenfolge in der Liste, die mit beginnt T, ein Element "Boo" folgt, während jede Zeichenfolge, die mit "U" beginnt, vollständig gelöscht wird. Ein optimaler Ansatz wäre wahrscheinlich so etwas wie:

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
}

Es wäre hilfreich gewesen, wenn List<T>eine Methode zum Erstellen einer Liste aus einem Teil eines Arrays bereitgestellt worden wäre , aber mir ist keine effiziente Methode dafür bekannt. Trotzdem sind Operationen an Arrays ziemlich schnell. Bemerkenswert ist die Tatsache, dass zum Hinzufügen und Entfernen von Elementen zur Liste kein "Verschieben" anderer Elemente erforderlich ist. Jedes Element wird direkt an die entsprechende Stelle im Array geschrieben.


1

Sie sollten wirklich for()statt foreach()in diesem Fall verwenden.


Dadurch wird möglicherweise Code mit einer weiteren Variablen überladen, die die ursprüngliche Anzahl der Elemente in der Sammlung enthält.
Anton Gogolev

2
Wenn Sie die Länge der Sammlung ändern, möchten Sie, dass Ihre for-Schleife anhand der neuen Länge und nicht anhand des Originals bewertet wird.
Andernfalls

@Anton: Sie benötigen diese zusätzliche Variable, da Sie die Iteration selbst verwalten müssen, wenn die Sammlung geändert wird
Rik

@Nippysaurus: Hört sich toll an und ich stimme Ihnen theoretisch zu, aber einige Sammlungen können nicht indiziert werden. Sie müssen über sie iterieren.
Josh G

1

Um Timos Antwort zu ergänzen, kann LINQ auch folgendermaßen verwendet werden:

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

Ich habe einen einfachen Schritt geschrieben, aber aufgrund dieser Leistung wird verschlechtert

Hier ist mein Code-Snippet: -

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;
                                }
                            }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.