Каков наилучший способ изменить список в цикле «foreach»? - PullRequest
65 голосов
/ 17 апреля 2009

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

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

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

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

Ответы [ 11 ]

66 голосов
/ 17 апреля 2009

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

Как сказано в MSDN :

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

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

15 голосов
/ 17 апреля 2009

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

foreach(var item in Enumerable)
{
    foreach(var item2 in item.Enumerable.ToList())
    {
        item.Add(item2)
    }
}
8 голосов
/ 29 апреля 2009

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

foreach(var item in collection.ToArray())
    collection.Add(new Item...);
7 голосов
/ 26 августа 2015

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

Для работающего примера:

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

4 голосов
/ 17 апреля 2009

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

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)));
1 голос
/ 27 января 2013

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

1 голос
/ 17 апреля 2009

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

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

Либо:

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

или

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

0 голосов
/ 30 октября 2018

Я написал один простой шаг, но из-за этого производительность будет ухудшаться

Вот мой фрагмент кода: -

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 голосов
/ 21 сентября 2018

Чтобы добавить к ответу Тимо, 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);
...