Каков наилучший способ изменить список в цикле "foreach"?

новая функция в C# / .NET 4.0 заключается в том, что вы можете изменить свой перечисляемый в foreach без исключения. См. запись в блоге Пола Джексона интересный побочный эффект параллелизма: удаление элементов из коллекции при перечислении для получения информации об этом изменении.

каков наилучший способ сделать следующее?

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

обычно я использую IList в качестве кэша / буфера до конца foreach, но там лучше способ?

10 ответов


коллекция, используемая в foreach, неизменна. Это очень много по дизайну.

Как говорится в MSDN:

оператор foreach используется для перебираем коллекцию получить информация, которую вы хотите, но можете не используется для добавления или удаления элементов из исходной коллекции, чтобы избежать непредсказуемые побочные эффекты. если вы нужно добавить или удалить элементы из коллекция источнику, использовать для петля.

должности в ссылке предоставленный Poko указывает, что это разрешено в новых параллельных коллекциях.


сделайте копию перечисления, используя в этом случае метод расширения IEnumerable, и перечислите его. Это добавило бы копию каждого элемента в каждом внутреннем перечислении к этому перечислению.

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

Как уже упоминалось, но с образцом кода:

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

чтобы проиллюстрировать ответ Nippysaurus: если вы собираетесь добавить новые элементы в список и хотите обработать вновь добавленные элементы тоже во время того же перечисления, то вы можете просто использовать на петли вместо foreach цикл, проблема решена :)

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

для примера runnable:

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

вывод последнего примера:

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

список (15 наименований)
0
1
2
3
4
5
6
7
8
9
1
3
5
7
9


вот как вы можете это сделать (быстрое и грязное решение. Если ты ... --3-->действительно нужно такое поведение, вы должны либо пересмотреть свой дизайн, либо переопределить все IList<T> члены и агрегировать список источников):

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

LINQ очень эффективен для жонглирования коллекциями.

ваши типы и структура мне неясны, но я постараюсь соответствовать вашему примеру в меру своих возможностей.

из вашего кода кажется, что для каждого элемента вы добавляете к этому элементу все из своего собственного свойства "Enumerable". Это очень просто:

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

в качестве более общего примера предположим, что мы хотим выполнить итерацию коллекции и удалить элементы, где условие истинно. Избегая foreach, используя LINQ:

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

добавить элемент на основе каждого существующего элемента? Нет проблем:

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

вы не можете изменить перечисляемую коллекцию во время ее перечисления, поэтому вам придется внести изменения до или после перечисления.

на for loop-хорошая альтернатива, но если ваш IEnumerable коллекция не реализует ICollection, это невозможно.

либо:

1) Сначала скопируйте коллекцию. Перечислите скопированную коллекцию и измените исходную коллекцию во время перечисления. (@tvanfosson)

или

2) сохраните список изменений и зафиксируйте их после перечисления.


лучший подход с точки зрения производительности, вероятно, использовать один или два массива. Скопируйте список в массив, выполните операции над массивом, а затем создайте новый список из массива. Доступ к элементу массива быстрее, чем доступ к элементу списка, и преобразования между List<T> и T[] можно использовать быструю операцию "массового копирования", которая позволяет избежать накладных расходов, связанных с доступом к отдельным элементам.

например, предположим, что у вас есть List<string> и желаю иметь каждый строка в списке, который начинается с T следует элемент "Boo", в то время как каждая строка, начинающаяся с" U", полностью отбрасывается. Оптимальный подход, вероятно, будет чем-то вроде:

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
}

было бы полезно, если List<T> предоставил метод для построения списка из части массива, но я не знаю никакого эффективного метода для этого. Тем не менее, операции с массивами довольно быстрые. Следует отметить, что добавление и удаление элементов из списка не требуют "тужиться" вокруг других элементов; каждый элемент записывается непосредственно в соответствующее место в массиве.


вы действительно должны использовать for() вместо foreach() в этом случае.


чтобы добавить к ответу Тимо, LINQ можно использовать так:

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