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

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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

Чтобы проиллюстрировать ответ 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

Ответ 5

Вот как вы можете это сделать (быстрое и грязное решение. Если вам действительно нужно такое поведение, вы должны либо пересмотреть свой дизайн, либо переопределить все члены 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);
        }
    }
}

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

Либо:

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

или

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

Ответ 9

Лучший подход с точки зрения производительности - это, вероятно, использование одного или двух массивов. Скопируйте список в массив, выполните операции над массивом, а затем создайте новый список из массива. Доступ к элементу массива происходит быстрее, чем доступ к элементу списка, а конверсии между 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> предоставил метод для построения списка из части массива, но я не знаю о каком-либо эффективном методе для этого. Тем не менее, операции с массивами довольно быстрые. Следует отметить, что добавление и удаление элементов из списка не требует "нажатия" вокруг других элементов; каждый элемент записывается непосредственно в соответствующее место в массиве.