Як видалити елементи із загального списку під час ітерації над ним?


451

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

Ви не можете використовувати .Remove(element)всередині foreach (var element in X)(бо це призводить до Collection was modified; enumeration operation may not execute.виключенню) ... Ви також не можете використовувати for (int i = 0; i < elements.Count(); i++)і .RemoveAt(i)тому що він руйнує вашу поточну позицію в колекції щодо i.

Чи є елегантний спосіб це зробити?

Відповіді:


734

Ітерації списку зворотно циклом for:

for (int i = safePendingList.Count - 1; i >= 0; i--)
{
    // some code
    // safePendingList.RemoveAt(i);
}

Приклад:

var list = new List<int>(Enumerable.Range(1, 10));
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i] > 5)
        list.RemoveAt(i);
}
list.ForEach(i => Console.WriteLine(i));

Крім того, ви можете використовувати метод RemoveAll з присудком для перевірки на:

safePendingList.RemoveAll(item => item.Value == someValue);

Ось спрощений приклад для демонстрації:

var list = new List<int>(Enumerable.Range(1, 10));
Console.WriteLine("Before:");
list.ForEach(i => Console.WriteLine(i));
list.RemoveAll(i => i > 5);
Console.WriteLine("After:");
list.ForEach(i => Console.WriteLine(i));

19
Для тих, хто надходить з Java, список C # схожий на ArrayList, що вставка / видалення - O (n), а пошук через індекс - O (1). Це не традиційний зв'язаний список. Здається, трохи невдало C # використовує слово "Список", щоб описати цю структуру даних, оскільки вона пам'ятає класичний пов'язаний список.
Джеррод Сміт

79
Ніщо в назві "Список" не пише "LinkedList". Люди, які походять з інших мов, ніж Java, можуть заплутатися, коли це буде пов'язаний список.
GolezTrol

5
Я опинився тут через пошук vb.net, тому, якщо хтось захоче еквівалентний синтаксис vb.net для RemoveAll:list.RemoveAll(Function(item) item.Value = somevalue)
pseudocoder

1
Це також працює на Java з мінімальною модифікацією: .size()замість .Countі .remove(i)замість .removeAt(i). Розумний - спасибі!
Осінній Леонард

2
Я зробив невеликий тест на продуктивність, і виявилося, що RemoveAll() займає втричі більше часу, ніж зворотний forцикл. Тож я безумовно дотримуюся петлі, принаймні у розділах, де це має значення.
Crusha K. Rool

84

Просте і зрозуміле рішення:

Використовуйте стандартний цикл для циклу, який працює назад, і RemoveAt(i)видаліть елементи.


2
Майте в виду , що видалення окремих записів час НЕ ефективно , якщо ваш список містить багато елементів. Він має потенціал бути O (n ^ 2). Уявіть список з двома мільярдами елементів, де просто трапляється, що перший мільярд елементів у кінцевому підсумку видаляється. Кожне видалення змушує копіювати всі більш пізні елементи, тому ви в кінцевому підсумку копіюєте мільярд елементів у мільярд разів кожен. Це не через зворотну ітерацію, це через одноразове видалення. RemoveAll гарантує, що кожен елемент буде скопійований не більше одного разу, щоб він був лінійним. Одноразове видалення може бути в мільярд разів повільніше. O (n) проти O (n ^ 2).
Брюс Доусон

71

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

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

ICollection<int> test = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

foreach (int myInt in test.Reverse<int>())
{
    if (myInt % 2 == 0)
    {
        test.Remove(myInt);
    }
}

7
Це прекрасно працювало для мене. Простий та елегантний і вимагає мінімальних змін до мого коду.
Стівен Макдугалл

1
Це просто геніальний фліппін? І я погоджуюся з @StephenMacDougall, мені не потрібно використовувати ці C ++ 'y для циклів, а просто вступайте в LINQ за призначенням.
Пьотр Кула

5
Я не бачу жодної переваги перед простим foreach (int myInt в test.ToList ()) {if (myInt% 2 == 0) {test.Remove (myInt); }} Вам ще потрібно виділити копію для Reverse, і вона вводить так? момент - чому там Реверс.
jahav

11
@jedesah Так, Reverse<T>()створюється ітератор, який переходить через список назад, але він виділяє додатковий буфер такого ж розміру, як і сам список для нього ( referenceource.microsoft.com/#System.Core/System/Linq/… ). Reverse<T>не просто проходить через оригінальний список у зворотному порядку (без виділення додаткової пам'яті). Тому обидва ToList()і Reverse()мають однакове споживання пам'яті (обидва створюють копію), але ToList()нічого не робить з даними. З Reverse<int>(), мені було б цікаво, чому список обернений, з якої причини.
jahav

1
@jahav Я бачу вашу думку. Це дуже невтішно, що реалізація Reverse<T>()створює новий буфер, я не зовсім впевнений, я розумію, чому це необхідно. Мені здається, що залежно від основної структури Enumerable, то, принаймні, в деяких випадках можна досягти зворотної ітерації без розподілу лінійної пам'яті.
jedesah

68
 foreach (var item in list.ToList()) {
     list.Remove(item);
 }

Якщо ви додасте ".ToList ()" до свого списку (або результати запиту LINQ), ви можете видалити "елемент" безпосередньо зі "списку" без страшного " Колекція була змінена; операція перерахування може не виконати ." помилка. Компілятор робить копію "списку", так що ви можете сміливо робити видалення з масиву.

Хоча ця модель не надто ефективна, вона має природне відчуття і є досить гнучкою для майже будь-якої ситуації . Наприклад, коли ви хочете зберегти кожен "елемент" у БД та видалити його зі списку лише тоді, коли збереження БД вдалося.


6
Це найкраще рішення, якщо ефективність не є вирішальною.
ІванП

2
це також швидше і читабельніше: list.RemoveAll (i => true);
santiago arizti

1
@Greg Little, чи я вас правильно зрозумів - коли ви додаєте компілятор ToList (), він проходить через скопійовану колекцію, але видаляє з оригіналу?
Pyrejkee

Якщо ваш список містить повторювані елементи, і ви хочете видалити лише цей пункт, який не відбудеться пізніше, чи не буде це видалення неправильним?
Тім МБ

Це також працює для списку об’єктів?
Том ель Сафаді

23

Виберіть потрібні елементи , а не намагайтеся видалити елементи, які ви не хочете. Це набагато простіше (і взагалі більш ефективно), ніж видаляти елементи.

var newSequence = (from el in list
                   where el.Something || el.AnotherThing < 0
                   select el);

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

Особисто я ніколи не видаляв елементи один за одним, якщо вам потрібно видалення, тоді дзвоніть, RemoveAllякий має предикат і лише один раз переставляє внутрішній масив, тоді як Removeвиконує Array.Copyоперацію з кожним видаленим вами елементом. RemoveAllнабагато ефективніше.

І коли ви переймете назад за списком, у вас вже є індекс елемента, який ви хочете видалити, тому було б набагато ефективніше дзвонити RemoveAt, оскількиRemove спочатку робиться обхід списку, щоб знайти індекс елемента, який ви Ви намагаєтесь видалити, але ви вже знаєте цей індекс.

Так що загалом я не бачу причин ніколи телефонувати Remove в циклі for. І в ідеалі, якщо це взагалі можливо, використовуйте вищевказаний код, щоб струмувати елементи зі списку за необхідністю, щоб взагалі не створювати другу структуру даних.


1
Або це, або додайте вказівник на небажані елементи до другого списку, після закінчення циклу повторіть список видалення та використовуйте його для видалення елементів.
Майкл Діллон

22

Використання ToArray () у загальному списку дозволяє зробити Remove (елемент) у вашому загальному списку:

        List<String> strings = new List<string>() { "a", "b", "c", "d" };
        foreach (string s in strings.ToArray())
        {
            if (s == "b")
                strings.Remove(s);
        }

2
Це не є помилковим, але я маю зазначити, що це обминає необхідність створення 2-го "зберігання" списку елементів, які ви хочете видалити, за рахунок копіювання всього списку до масиву. Другий список підібраних вручну елементів, ймовірно, матиме менше предметів.
Ахмад Магед

20

Використання .ToList () зробить копію вашого списку, як це пояснено в цьому запитанні: ToList () - чи створює новий список?

Використовуючи ToList (), ви можете видалити зі свого оригінального списку, оскільки ви насправді переглядаєте копію.

foreach (var item in listTracked.ToList()) {    

        if (DetermineIfRequiresRemoval(item)) {
            listTracked.Remove(item)
        }

     }

1
Але з точки зору продуктивності ви копіюєте свій список, що може зайняти деякий час. Хороший і простий спосіб зробити це, але з не дуже хорошим виконанням
Флоріан К

12

Якщо функція, яка визначає, які елементи для видалення не має побічних ефектів, і не мутує елемент (це чиста функція), просте та ефективне (лінійний час) рішення:

list.RemoveAll(condition);

Якщо є побічні ефекти, я б використав щось на кшталт:

var toRemove = new HashSet<T>();
foreach(var item in items)
{
     ...
     if(condition)
          toRemove.Add(item);
}
items.RemoveAll(toRemove.Contains);

Це ще лінійний час, якщо припустити, що хеш хороший. Але у нього збільшується використання пам’яті завдяки хештету.

Нарешті, якщо ваш список є лише IList<T>замість того, List<T>я пропоную свою відповідь на те, як я можу зробити цей спеціальний ітератор передбачення? . Це матиме лінійний час виконання за типовими реалізаціями IList<T>, порівняно з квадратичним виконанням багатьох інших відповідей.


11

Оскільки будь-яке видалення здійснюється за умови, яке ви можете використовувати

list.RemoveAll(item => item.Value == someValue);

Це найкраще рішення, якщо обробка не мутує елемент і не має побічних ефектів.
CodesInChaos


8

Ви не можете використовувати foreach, але ви можете ітератувати вперед і керувати змінною індексу циклу, коли ви виймаєте елемент, наприклад:

for (int i = 0; i < elements.Count; i++)
{
    if (<condition>)
    {
        // Decrement the loop counter to iterate this index again, since later elements will get moved down during the remove operation.
        elements.RemoveAt(i--);
    }
}

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


6

Використання Removeабо RemoveAtв списку під час повторного повторення цього списку навмисно ускладнюється, оскільки це майже завжди неправильно робити . Можливо, ви зможете змусити це працювати якоюсь хитрою хитрістю, але це буде дуже повільно. Кожен раз, коли ви Removeїї телефонуєте, доводиться переглядати весь список, щоб знайти елемент, який потрібно видалити. Кожен раз, коли ви RemoveAtїї телефонуєте , потрібно переміщувати наступні елементи 1 вліво. Таким чином, будь-яке рішення, що використовує Removeабо RemoveAtвимагає квадратичного часу, O (n²) .

Використовуйте, RemoveAllякщо можете. В іншому випадку наступний шаблон буде фільтрувати список на місці за лінійним часом, O (n) .

// Create a list to be filtered
IList<int> elements = new List<int>(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
// Filter the list
int kept = 0;
for (int i = 0; i < elements.Count; i++) {
    // Test whether this is an element that we want to keep.
    if (elements[i] % 3 > 0) {
        // Add it to the list of kept elements.
        elements[kept] = elements[i];
        kept++;
    }
}
// Unfortunately IList has no Resize method. So instead we
// remove the last element of the list until: elements.Count == kept.
while (kept < elements.Count) elements.RemoveAt(elements.Count-1);

3

Я б хотів, щоб «візерунок» був приблизно таким:

foreach( thing in thingpile )
{
    if( /* condition#1 */ )
    {
        foreach.markfordeleting( thing );
    }
    elseif( /* condition#2 */ )
    {
        foreach.markforkeeping( thing );
    }
} 
foreachcompleted
{
    // then the programmer's choices would be:

    // delete everything that was marked for deleting
    foreach.deletenow(thingpile); 

    // ...or... keep only things that were marked for keeping
    foreach.keepnow(thingpile);

    // ...or even... make a new list of the unmarked items
    others = foreach.unmarked(thingpile);   
}

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


1
Досить просто. Просто створіть буловий масив прапорів (використовуйте 3-штатний тип, наприклад Nullable<bool>, якщо ви хочете дозволити маркування), а потім використовуйте його після передбачення для видалення / збереження елементів.
Ден Бешард

3

Припускаючи, що предикат - булева властивість елемента, що якщо це правда, то елемент слід видалити:

        int i = 0;
        while (i < list.Count())
        {
            if (list[i].predicate == true)
            {
                list.RemoveAt(i);
                continue;
            }
            i++;
        }

Я дав цю заяву, оскільки іноді може бути ефективніше рухатись через Список у порядку (а не в зворотному порядку). Можливо, ви могли зупинитися, коли не знайдете перший елемент, який не слід видаляти, оскільки список впорядкований. (Уявіть собі "перерву", де i ++ є у цьому прикладі.
FrankKrumnow

3

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

list = list.Where(item => ...).ToList();

Якщо список не дуже великий, у цьому не повинно виникнути суттєвих проблем.


3

Найкращий спосіб видалити елементи зі списку під час ітерації над ним - це використовувати RemoveAll() . Але головне занепокоєння, яке пишуть люди, полягає в тому, що вони повинні робити якісь складні речі всередині циклу та / або мати складні випадки порівняння.

Рішення все-таки використовувати, RemoveAll()але використовувати це позначення:

var list = new List<int>(Enumerable.Range(1, 10));
list.RemoveAll(item => 
{
    // Do some complex operations here
    // Or even some operations on the items
    SomeFunction(item);
    // In the end return true if the item is to be removed. False otherwise
    return item > 5;
});

2
foreach(var item in list.ToList())

{

if(item.Delete) list.Remove(item);

}

Просто створіть абсолютно новий список з першого. Я кажу "Легко", а не "Правильно", оскільки створення абсолютно нового списку, ймовірно, приносить премію за ефективність за попередній метод (я не переймався жодним бенчмаркінг.) Я, як правило, віддаю перевагу цій схемі, вона також може бути корисною для подолання Обмеження Linq-To-Entities.

for(i = list.Count()-1;i>=0;i--)

{

item=list[i];

if (item.Delete) list.Remove(item);

}

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


1

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

var ids = new List<int> { 1, 2, 3, 4 };
var iterableIds = ids.ToList();

Parallel.ForEach(iterableIds, id =>
{
    ids.Remove(id);
});

1

Я б так зробив

using System.IO;
using System;
using System.Collections.Generic;

class Author
    {
        public string Firstname;
        public string Lastname;
        public int no;
    }

class Program
{
    private static bool isEven(int i) 
    { 
        return ((i % 2) == 0); 
    } 

    static void Main()
    {    
        var authorsList = new List<Author>()
        {
            new Author{ Firstname = "Bob", Lastname = "Smith", no = 2 },
            new Author{ Firstname = "Fred", Lastname = "Jones", no = 3 },
            new Author{ Firstname = "Brian", Lastname = "Brains", no = 4 },
            new Author{ Firstname = "Billy", Lastname = "TheKid", no = 1 }
        };

        authorsList.RemoveAll(item => isEven(item.no));

        foreach(var auth in authorsList)
        {
            Console.WriteLine(auth.Firstname + " " + auth.Lastname);
        }
    }
}

ВИХІД

Fred Jones
Billy TheKid

0

Я опинився в подібній ситуації, коли мені довелося видаляти кожен n- й елемент у заданій List<T>.

for (int i = 0, j = 0, n = 3; i < list.Count; i++)
{
    if ((j + 1) % n == 0) //Check current iteration is at the nth interval
    {
        list.RemoveAt(i);
        j++; //This extra addition is necessary. Without it j will wrap
             //down to zero, which will throw off our index.
    }
    j++; //This will always advance the j counter
}

0

Вартість вилучення предмета зі списку пропорційна кількості предметів, що слідують за вилученим. У випадку, коли перша половина елементів відповідає вимогам для вилучення, будь-який підхід, заснований на вилученні елементів окремо, в кінцевому підсумку повинен виконати близько N * N / 4 операцій з копіювання предмета, які можуть стати дуже дорогими, якщо список великий .

Більш швидкий підхід полягає в перегляді списку, щоб знайти перший елемент, який потрібно видалити (якщо такий є), а потім з цього моменту скопіюйте кожен елемент, який слід зберегти до місця, де він належить. Після цього, якщо R елементів слід зберегти, першими R елементами у списку будуть R-елементи, а всі елементи, що потребують видалення, будуть наприкінці. Якщо ці елементи буде видалено у зворотному порядку, системі не доведеться копіювати будь-який з них, тому якщо в списку було N елементів, з яких R елементів, у тому числі всіх перших F, було збережено, необхідно буде копіюйте РФ-елементи та скорочуйте список на один NR разів. Весь лінійний час.


0

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

var messageList = ...;
// Restrict your list to certain criteria
var customMessageList = messageList.FindAll(m => m.UserId == someId);

if (customMessageList != null && customMessageList.Count > 0)
{
    // Create list with positions in origin list
    List<int> positionList = new List<int>();
    foreach (var message in customMessageList)
    {
        var position = messageList.FindIndex(m => m.MessageId == message.MessageId);
        if (position != -1)
            positionList.Add(position);
    }
    // To be able to remove the items in the origin list, we do it backwards
    // so that the order of indices stays the same
    positionList = positionList.OrderByDescending(p => p).ToList();
    foreach (var position in positionList)
    {
        messageList.RemoveAt(position);
    }
}

0

У C # один простий спосіб - позначити ті, які ви хочете видалити, а потім створити новий список для повторення ...

foreach(var item in list.ToList()){if(item.Delete) list.Remove(item);}  

або навіть простіше використовувати linq ....

list.RemoveAll(p=>p.Delete);

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


0

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

using System.Linq;

List<MyProperty> _Group = new List<MyProperty>();
// ... add elements

bool cond = true;
foreach (MyProperty currObj in _Group)
{
    if (cond) 
    {
        // SET - element can be deleted
        currObj.REMOVE_ME = true;
    }
}
// RESET
_Group.RemoveAll(r => r.REMOVE_ME);

0

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

ArrayList place_holder = new ArrayList();
place_holder.Add("1");
place_holder.Add("2");
place_holder.Add("3");
place_holder.Add("4");

for(int i = place_holder.Count-1; i>= 0; i--){
    if(i>= place_holder.Count){
        i = place_holder.Count-1; 
    }

// some method that removes multiple elements here
}

-4
myList.RemoveAt(i--);

simples;

1
Що simples;тут роблять?
Денні

6
його делегат .. Він спростовує мою відповідь кожного разу, коли вона працює
SW

SW ROFL! Треба любити ваш коментар.
Ерік Браун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.