Який найкращий спосіб змінити список у циклі «foreach»?


79

Нова функція в C # / .NET 4.0 полягає в тому, що ви можете змінити свої перелічені в a foreachбез отримання винятку. Інформацію про цю зміну див. У записі блогу Пола Джексона “ Цікавий побічний ефект паралельності: вилучення предметів з колекції під час перерахування” .

Який найкращий спосіб зробити наступне?

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

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


2
Хм .. Чи можете ви вказати нам документацію щодо цієї зміни? Перерахування завжди були незмінними при переліченні над колекцією.
CraigTP

Це варіація теми, яка спонукала Стіва Макконнелла порадити ніколи не мавпувати з індексом циклу .
Ед Гінес

4
Я знаю, що розкопую тут дуже давню розмову, але я був би з цим дуже обережний. Лише нові паралельні колекції можуть бути змінені в межах foreach - всі попередні типи колекцій, і, я думаю, більшість майбутніх типів колекцій все ще залишаться незмінними при перерахуванні над ними. Широке використання цієї химерності фактично заблокує вас у використанні одночасних колекцій, тому що, якщо ви захочете використовувати іншу колекцію в майбутньому, всі ваші химерні цикли foreach раптово порвуться.
Кріс


Це питання задає конкретно супутні колекції? Або це ставить більш загальне питання і згадує це лише для протиставлення?
UuDdLrLrS

Відповіді:


80

Колекція, що використовується в foreach, є незмінною. Це дуже багато за задумом.

Як сказано на MSDN :

Заява foreach використовується для перегляду колекції, щоб отримати потрібну інформацію, але не може використовуватися для додавання або видалення елементів із вихідної колекції, щоб уникнути непередбачуваних побічних ефектів. Якщо вам потрібно додати або видалити елементи з вихідної колекції, використовуйте цикл for.

Допис у посиланні, наданому Poko, вказує, що це дозволено в нових одночасних колекціях.


22
Ваша відповідь насправді не відповідає на питання. Він знає, що основний цикл foreach незмінний ... він хоче знати найкращий спосіб застосувати зміни до незліченної колекції.
Джош Г

13
Тоді відповідь така: використовуйте звичайний цикл for, як пропонується у цитаті. Мені відомо, що OP згадує про зміни у поведінці в C # 4.0, але я не можу знайти жодної інформації щодо цього. На даний момент, я думаю, це все ще відповідна відповідь.
Рік,

14

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

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

1
Але навіщо копіювати перелічуване у список замість масиву? Список містить методи пошуку, сортування та маніпулювання колекцією, які нам не потрібні.
Rudey

7

Як уже згадувалося, але із зразком коду:

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

Це не є чудовою відповіддю, оскільки вона виконує непотрібні розподіли та копії ( ToArray()) просто для перегляду списку.
antiduh

7

Для ілюстрації відповіді Nippysaurus: Якщо ви збираєтеся додати нові елементи до списку і хочете також обробити нещодавно додані елементи під час одного і того ж перерахування, ви можете просто використовувати for замість циклу 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


3

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

forПетля є хорошою альтернативою, але якщо ваша IEnumerableколекція не реалізує ICollection, що не представляється можливим.

Або:

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

або

2) Зберігайте список змін і фіксуйте їх після перерахування.


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

3

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

Найкращий підхід з точки зору продуктивності - це, мабуть, використання одного або двох масивів. Скопіюйте список у масив, виконайте операції з ним, а потім створіть новий список із масиву. Доступ до елемента масиву відбувається швидше, ніж доступ до елемента списку, і перетворення між a List<T>та a 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

Вам слід використовувати справді for()замість foreach()цього.


Це може потенційно захарастити код ще однією змінною, яка міститиме початкову кількість елементів у колекції.
Антон Гоголєв

2
Якщо ви змінюєте довжину колекції, тоді ви хочете, щоб ваш цикл for оцінювався за новою довжиною, а не за оригіналом, або ви могли б отримати виняток за межі діапазону
cjk,

@Anton: Вам знадобиться ця додаткова змінна, оскільки вам доведеться самостійно керувати ітерацією, коли колекція модифікується
Рік,

@Nippysaurus: Звучить чудово, і я погоджусь з вами в теорії, але деякі колекції неможливо проіндексувати. Ви повинні переглядати їх.
Джош Г

1

Щоб додати до відповіді Тимо, 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);

0

Я написав один простий крок, але через це продуктивність буде погіршуватися

Ось мій фрагмент коду: -

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;
                                }
                            }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.