Розумний спосіб видалення елементів зі списку <T> під час перерахування на C #


87

У мене є класичний випадок спроби видалити елемент із колекції, перераховуючи його в циклі:

List<int> myIntCollection = new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

foreach (int i in myIntCollection)
{
    if (i == 42)
        myIntCollection.Remove(96);    // The error is here.
    if (i == 25)
        myIntCollection.Remove(42);    // The error is here.
}

На початку ітерації після того, як відбулася зміна, InvalidOperationExceptionкидається an , тому що перечислювачі не люблять, коли базова колекція змінюється.

Мені потрібно внести зміни до колекції під час ітерації. Існує багато шаблонів, які можна використовувати, щоб уникнути цього , але, здається, жоден з них не має гарного рішення:

  1. Не видаляйте всередині цього циклу, натомість тримайте окремий “Видалити список”, який ви обробляєте після основного циклу.

    Зазвичай це хороше рішення, але в моєму випадку мені потрібно, щоб елемент негайно зник, оскільки “чекає”, поки основний цикл дійсно видалить елемент, що змінює логічний потік мого коду.

  2. Замість того, щоб видаляти елемент, просто встановіть прапор на ньому та позначте його як неактивний. Потім додайте функціональність шаблону 1, щоб очистити список.

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

  3. Якось включіть ідеї зразка 2 у клас, який походить від List<T>. Цей Суперліст буде обробляти неактивний прапор, видалення об’єктів після факту, а також не буде виставляти елементи, позначені як неактивні, для переліку споживачів. По суті, він просто інкапсулює всі ідеї зразка 2 (і згодом зразка 1).

    Чи існує такий клас? Хтось має код для цього? Або є кращий спосіб?

  4. Мені сказали, що доступ myIntCollection.ToArray()замість myIntCollectionвирішить проблему і дозволить мені видалити всередині циклу.

    Мені це здається поганим шаблоном дизайну, а може, це добре?

Подробиці:

  • Список буде містити багато елементів, і я буду вилучати лише деякі з них.

  • Усередині циклу я буду виконувати всілякі процеси, додавати, видаляти тощо, тому рішення має бути досить загальним.

  • Елемент, який мені потрібно видалити, може не бути поточним елементом у циклі. Наприклад, я можу потрапити до пункту 10 циклу з 30 елементів, і мені потрібно видалити елемент 6 або пункт 26. Проходження назад через масив через це більше не працюватиме. ; o (


Можлива корисна інформація для когось іншого: Помилка модифікації Avoid Collection (інкапсуляція шаблону 1)
Джордж Дакетт

Додаткова примітка: Списки витрачають багато часу (зазвичай O (N), де N - довжина списку) на переміщення значень. Якщо насправді потрібен ефективний довільний доступ, можна домогтися видалення в O (журнал N), використовуючи збалансоване двійкове дерево, що містить кількість вузлів у піддереві, коренем якого він є. Це BST, ключ якого (індекс у послідовності) мається на увазі.
Palec 01.03.16

Будь ласка , дивіться відповідь: stackoverflow.com/questions/7193294 / ...
Даббас

Відповіді:


196

Найкращим рішенням зазвичай є використання RemoveAll()методу:

myList.RemoveAll(x => x.SomeProp == "SomeValue");

Або, якщо вам потрібно видалити певні елементи:

MyListType[] elems = new[] { elem1, elem2 };
myList.RemoveAll(x => elems.Contains(x));

Це припускає, що ваш цикл призначений виключно для цілей видалення, звичайно. Якщо ви робите необхідність додаткової обробки, то кращий спосіб, як правило , використовувати forабо whileпетлю, з тих пір ви не використовуєте нумератор:

for (int i = myList.Count - 1; i >= 0; i--)
{
    // Do processing here, then...
    if (shouldRemoveCondition)
    {
        myList.RemoveAt(i);
    }
}

Повернення назад гарантує, що ви не пропустите жодних елементів.

Відповідь на редагування :

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

List<int> toRemove = new List<int>();
foreach (var elem in myList)
{
    // Do some stuff

    // Check for removal
    if (needToRemoveAnElement)
    {
        toRemove.Add(elem);
    }
}

// Remove everything here
myList.RemoveAll(x => toRemove.Contains(x));

Щодо Вашої відповіді: Мені потрібно негайно видалити елементи під час обробки цього елемента, не після того, як буде оброблено весь цикл. Рішення, яке я використовую, полягає в тому, щоб НУЛЯТИ будь-які елементи, які я хочу негайно видалити, а потім видалити їх. Це не ідеальне рішення, оскільки мені доводиться перевіряти наявність НУЛЮ повсюдно, але це ПРАЦЮЄ.
Джон Сток,

Цікаво, якщо 'elem' не є int, тоді ми не можемо використовувати RemoveAll таким чином, що він присутній у відредагованому коді відповіді.
Користувач M

22

Якщо вам обом потрібно перерахувати a List<T>і видалити з нього, то я пропоную просто використовувати whileцикл замість aforeach

var index = 0;
while (index < myList.Count) {
  if (someCondition(myList[index])) {
    myList.RemoveAt(index);
  } else {
    index++;
  }
}

На мою думку, це має бути прийнятою відповіддю. Це дозволяє розглянути решту елементів у вашому списку, не повторюючи шорт-лист елементів, які потрібно вилучити.
Slvrfn

13

Я знаю, що цей пост старий, але я думав поділитися тим, що мені вдалося.

Створіть копію списку для перерахування, а потім у кожному циклі ви зможете обробляти скопійовані значення та видаляти / додавати / будь-що разом із вихідним списком.

private void ProcessAndRemove(IList<Item> list)
{
    foreach (var item in list.ToList())
    {
        if (item.DeterminingFactor > 10)
        {
            list.Remove(item);
        }
    }
}

Чудова ідея щодо ".ToList ()"! Простий шльопанець, а також працює у випадках, коли ви не використовуєте запас "Видалити ... ()" безпосередньо.
galaxis

1
Дуже неефективний.
nawfal

8

Коли вам потрібно переглядати список і, можливо, змінити його під час циклу, вам краще використовувати цикл for:

for (int i = 0; i < myIntCollection.Count; i++)
{
    if (myIntCollection[i] == 42)
    {
        myIntCollection.Remove(i);
        i--;
    }
}

Звичайно, ви повинні бути обережними, наприклад, я декрементую iкожен раз, коли елемент видаляється, інакше ми пропускатимемо записи (альтернативою є повернення назад за списком).

Якщо у вас є Linq, тоді вам слід просто використовувати, RemoveAllяк запропонував dlev.


Працює лише тоді, коли ви видаляєте поточний елемент. Якщо ви видаляєте довільний елемент, вам потрібно буде перевірити, чи був його індекс в / до або після поточного індексу, щоб вирішити, чи потрібно --i.
CompuChip

Початкове запитання не давало зрозуміти, що видалення інших елементів, крім поточного, має підтримуватися, @CompuChip. Ця відповідь не змінилася з часу її з’ясування.
Палець 01.03.16

@Palec, я розумію, звідси мій коментар.
CompuChip

5

Під час перерахування списку додайте той, який потрібно ЗБЕРІГАТИ, до нового списку. Потім призначте новий список дляmyIntCollection

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
List<int> newCollection=new List<int>(myIntCollection.Count);

foreach(int i in myIntCollection)
{
    if (i want to delete this)
        ///
    else
        newCollection.Add(i);
}
myIntCollection = newCollection;

3

Давайте додамо вам код:

List<int> myIntCollection=new List<int>();
myIntCollection.Add(42);
myIntCollection.Add(12);
myIntCollection.Add(96);
myIntCollection.Add(25);

Якщо ви хочете змінити список, перебуваючи на шляху, потрібно ввести текст .ToList()

foreach(int i in myIntCollection.ToList())
{
    if (i == 42)
       myIntCollection.Remove(96);
    if (i == 25)
       myIntCollection.Remove(42);
}

0

Як на рахунок

int[] tmp = new int[myIntCollection.Count ()];
myIntCollection.CopyTo(tmp);
foreach(int i in tmp)
{
    myIntCollection.Remove(42); //The error is no longer here.
}

У поточному C # його можна переписати як foreach (int i in myIntCollection.ToArray()) { myIntCollection.Remove(42); }для будь-якого переліку, і, List<T>зокрема, він підтримує цей метод навіть у .NET 2.0.
Палець 01.03.16

0

Якщо вас цікавить висока продуктивність, ви можете використовувати два списки. Далі зводиться до мінімуму збір сміття, максимізується локалізація пам’яті і ніколи фактично не вилучається елемент зі списку, що дуже неефективно, якщо це не останній елемент.

private void RemoveItems()
{
    _newList.Clear();

    foreach (var item in _list)
    {
        item.Process();
        if (!item.NeedsRemoving())
            _newList.Add(item);
    }

    var swap = _list;
    _list = _newList;
    _newList = swap;
}

0

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

    public static IList<T> RemoveAllKeepRemoved<T>(this IList<T> source, Predicate<T> predicate)
    {
        IList<T> removed = new List<T>();
        for (int i = source.Count - 1; i >= 0; i--)
        {
            T item = source[i];
            if (predicate(item))
            {
                removed.Add(item);
                source.RemoveAt(i);
            }
        }
        return removed;
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.