JavaRush /Курси /C# SELF /Видалення елементів колекції у циклі

Видалення елементів колекції у циклі foreach

C# SELF
Рівень 29 , Лекція 4
Відкрита

1. Вступ

Майже кожен, хто починає програмувати на C#, рано чи пізно стикається з однаковою проблемою: є колекція (наприклад, список об’єктів), і потрібно видалити з неї зайві елементи за певною умовою. Здається просто, і рука сама тягнеться до звичного, зручного циклу foreach, адже це «найбезпечніший» і «дружній» спосіб перебору. Та раптом, у найнесподіваніший момент, зʼявляється загадкова помилка часу виконання, якої не було на простих прикладах, — і робота програми зупиняється на рівному місці.

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

Чому foreach не сумісний із видаленням елементів

Щоб краще уявити, що відбувається, уявіть чергу людей (це наша колекція). Ви рухаєтеся чергою й питаєте кожного: «Залишити чи викреслити?» Якщо почати викреслювати когось прямо під час перебору, уся черга зсувається, люди переміщуються, і план «наступна людина — наступна у списку» миттєво ламається. Можливо, хтось залишиться не опитаним або когось ви запитаєте двічі.

Приклад на C#:


List<string> names = new List<string> { "Антон", "Борис", "Віка", "Гріша" };

foreach (string name in names)
{
    if (name.StartsWith("В"))
        names.Remove(name); // Бум! InvalidOperationException
}

Коли програма дійде до "Віка" і вирішить її видалити, внутрішній ітератор втратить «контакт із реальністю» — і ви отримаєте повідомлення:
InvalidOperationException: Collection was modified; enumeration operation may not execute.

Це не просто примха — так C# захищає вас від важковідтворюваних помилок і пошкодження структури даних.

2. Чому такий простий код не працює?

Як усе влаштовано всередині?

Коли ви пишете цикл foreach, компілятор генерує спеціальний об’єкт — ітератор (IEnumerator), який відстежує поточну позицію в колекції. Цей об’єкт пам’ятає, скільки елементів було на старті, який елемент зараз «активний», і суворо контролює, щоб колекцію не змінювали під час перебору.

Будь-яка спроба видалити або додати елемент у межах foreach порушує цей контракт. Чому? Якщо після видалення елементів індекси зсунулися, ітератор уже не зможе коректно перейти до наступного елемента. Хтось може бути пропущений, когось врахують двічі — у підсумку вийде безлад. Тому за першої ж зміни колекції .NET чесно й чітко викидає виняток.

До чого призводить видалення напряму

Уявімо, що ми написали таку програму:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
foreach (int x in numbers)
{
    if (x % 2 == 0)
        numbers.Remove(x);
}

Здавалося б, усе логічно: перебрати всі числа й видалити парні. Але на другому проході програма викине помилку — «колекцію змінено під час ітерації».

Іноді виникає спокуса обійти це обмеження й спробувати «на свій страх і ризик». Але навіть якби помилки не було, залежно від структури колекції результат був би непередбачуваним. Наприклад, ви могли б випадково «перестрибнути» через деякі елементи або видалити не все, що треба.

3. Як же правильно?

Техніка №1: Зворотний цикл for

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


List<string> names = new List<string> { "Антон", "Борис", "Віка", "Гріша" };

for (int i = names.Count - 1; i >= 0; i--)
{
    if (names[i].StartsWith("В"))
        names.RemoveAt(i);
}

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

Техніка №2: Відфільтрувати й створити новий список

Іноді простіше (і часто швидше) перебрати колекцію, зібрати лише ті елементи, які мають залишитися, і замінити початковий список новим.


var names = new List<string> { "Антон", "Борис", "Віка", "Гріша" };
names = names.Where(name => !name.StartsWith("В")).ToList();
// У результаті залишаться "Антон" і "Гріша"

Цей підхід доречний, коли колекція не надто велика або не критично зберегти початкове посилання на об’єкт.

Техніка №3: Використовувати спеціальні методи колекцій

Якщо ви працюєте з класичним List<T>, то для видалення за умовою є зручний метод:


names.RemoveAll(name => name.StartsWith("В"));

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

Техніка №4: Збирати на видалення

Є колекції, які не можна змінювати «на льоту» (наприклад, Dictionary, HashSet, або навіть ваш власний клас). У таких випадках застосовують підхід «позначити на видалення»:

  1. Спочатку пройдіться колекцією і зберіть усі елементи, які треба видалити, в окремий список.
  2. Потім пройдіться цим новим списком і видаліть потрібні елементи з початкової колекції.

Dictionary<int, string> dict = new Dictionary<int, string> { [1] = "one", [2] = "two", [3] = "three" };
var toDelete = new List<int>();
foreach (var kvp in dict)
{
    if (kvp.Key % 2 == 0)
        toDelete.Add(kvp.Key);
}
foreach (var key in toDelete)
    dict.Remove(key);

4. Корисні нюанси

Помилки й міфи початківців

Одна з найпоширеніших помилок — очікувати, що видалення елемента з колекції під час перебору спрацює «якось», адже в деяких інших мовах (наприклад, Python) це часто можливо. Але у C# це суворо заборонено саме для вашої безпеки: набагато краще отримати явний виняток, ніж тихий і підступний дефект, який потім ніхто не зможе відтворити.

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

Мораль історії

Завдання «видалити елементи з колекції за умовою» трапляється в кожній другій програмі на C#, але робити це прямо всередині циклу foreach не можна — така архітектура мови, продиктована турботою про цілісність ваших даних і відсутність неочікуваних помилок.
Запамʼятайте це правило, і воно позбавить вас безсонних ночей із налагоджувачем.

Як робити завжди правильно

  • Ніколи не видаляйте елементи колекції прямо в циклі foreach. Це спричинить помилку часу виконання.
  • Для списків (List<T>) і масивів використовуйте або цикл for з кінця, або методи RemoveAll і фільтрацію через LINQ.
  • Для словників, множин та інших складних колекцій — спочатку збирайте елементи на видалення, потім пройдіться цим списком і видаліть їх із початкової колекції.
  • Якщо не впевнені — поміркуйте: як змінюється колекція під час видалення? Як поводитиметься ітератор? Якщо є бодай найменший сумнів, спосіб обрано не той.
1
Опитування
Фільтрація елементів, рівень 29, лекція 4
Недоступний
Фільтрація елементів
Робота з колекціями
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ