Найкращий спосіб видалити предмети з колекції


74

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

//Remove the existing role assignment for the user.
int cnt = 0;
int assToDelete = 0;
foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments)
{
    if (spAssignment.Member.Name == shortName)
    {
        assToDelete = cnt;
    }
    cnt++;
}
workspace.RoleAssignments.Remove(assToDelete);

Що б я насправді хотів зробити, це знайти елемент для видалення за властивістю (в даному випадку за назвою), не переглядаючи всю колекцію та використовуючи 2 додаткові змінні.


78
Полюбіть імена змінних. Однак я б не хотів бути @ss, який видаляється.
tvanfosson,

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

Я думаю, ви мали на увазі RemoveAt () у своєму фрагменті коду, оскільки ви передаєте індекс. Як тільки товар стане відомим, ви можете зателефонувати безпосередньо Remove ().
хурст

Це питання слід прояснити. З якими .Net framework працюють відповіді? Ми говоримо про List <T> або якусь іншу структуру, що реалізує IList <T> - Це, мабуть, слід перейменувати на «Який найкращий спосіб видалити елементи зі списку <T> в .net 3.0?»
Сем Шафран

Відповіді:


28

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

В іншому випадку ви можете принаймні зробити це:

foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments)
{
    if (spAssignment.Member.Name == shortName)
    {
        workspace.RoleAssignments.Remove(spAssignment);
        break;
    }
}

13
Це може спричинити виняток, оскільки ви змінюєте колекцію під час її використання ...
RWendi,

14
Ні, це не так. Це тому, що після вилучення предмета є перерва.
jfs

25
Jusst на випадок, якщо хтось прочитає це і застосує його до ситуації, коли видаляється кілька елементів, збережіть декілька індексів у масив і використовуйте окремий цикл for, який циклично обертається через масив delete для видалення елементів.
Роберт К. Барт,

Або просто відфільтруйте за допомогою workspace.RoleAssignments = workspace.RoleAssignments.Where (x => x.Member.Name! = ShortName) .ToList () ;. Але я віддаю перевагу відповіді Ліама, коли це можливо.
Девід

138

Якщо RoleAssignments є a, List<T>ви можете використовувати наступний код.

workSpace.RoleAssignments.RemoveAll(x =>x.Member.Name == shortName);

Я думаю, це лише RemoveAll, що має параметр функції. Принаймні, я не можу створити його за допомогою Remove.
MichaelGG

5
Так, це RemoveAll. Я фактично витратив час, щоб перевірити це, переконався, що це RemoveAll і все ще вставив у Remove. Шкода, що у stackoverflow немає вбудованого компілятора :)
JaredPar

Це генерує новий список, якщо я не помиляюся.
Річард Ніенабер,

4
AFAIK це видалення на місці.
Джон Лімджап,

23

@smaclell запитав, чому зворотна ітерація була більш ефективною в коментарі @ sambo99.

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

Ми маємо такі дані

"Bob" 999
"Mary" 999
"Ted" 1000

Якби ми хотіли повторити вперед, то незабаром потрапимо в біду

for( int idx = 0; idx < list.Count ; idx++ )
{
    if( list[idx].Rating < 1000 )
    {
        list.RemoveAt(idx); // whoops!
    }
}

При idx = 0 ми видаляємо Bob, що потім зміщує всі інші елементи вліво. Наступного разу через цикл idx = 1, але список [1] ​​тепер Tedзамість Mary. Ми в кінцевому підсумку пропускаємо Maryпомилково. Ми могли б використати цикл while, і ми могли б ввести більше змінних.

Або ми просто повертаємо ітерацію назад:

for (int idx = list.Count-1; idx >= 0; idx--)
{
    if (list[idx].Rating < 1000)
    {
        list.RemoveAt(idx);
    }
}

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

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

Тепер ви можете просто використовувати Linq і прямо заявляти про те, що робите.

list.RemoveAll(o => o.Rating < 1000);

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

int removeIndex = list.FindIndex(o => o.Name == "Ted");
if( removeIndex != -1 )
{
    list.RemoveAt(removeIndex);
}

2
Для простого List <T>, якщо вам потрібно вилучити більше 1 елемента, зворотний цикл for - ЗАВЖДИ найефективніший спосіб зробити це. Це, безумовно, ефективніше, ніж копіювання даних у список listToRemove. Б'юся об заклад, реалізація Linq використовує той самий фокус.
Сем Шафран

@ sambo99 Я повністю згоден і намагаюся розширити вашу відповідь, пояснивши, чому ітерація вперед не завжди працює. Також я вказую, що коли не надається зайвої інформації, зворотна ітерація не є ні більш, ні менш ефективною, якщо ви видаляєте щонайбільше 1 запис. O (n) настільки ж добре, наскільки це вдається зі списками.
Роберт Полсон,

Yerp. Зараз я це виправляю ... List <T> має дуже неприємну реалізацію RemoveAt, найефективнішим способом, здається, є копіювання даних, які нам потрібні зі списку
Сем Саффрон,

Уявіть, що ви хочете, щоб ваші дані були видалені з лівого краю (скажімо, ви хочете видалити більшість елементів, але ви не знаєте, скільки). Очевидно, зворотне рішення - найгірший спосіб зробити це .... У вашому прикладі замість WHOOPSIE! ви повинні були написати list.RemoveAt (idx--); Альрігт?

@ Maxima120 ви маєте рацію, але якщо ви читаєте те , що я сказав, я намагався показати , чому зворотна ітерація може бути більш ефективним , більшість часу. Я не сперечаюся за його універсальність. Сьогодні я б не намагався повторювати себе, а віддав би перевагу версії LINQ.
Роберт Полсон,

11

Якщо це ICollectionтоді, у вас не буде RemoveAllметоду. Ось метод розширення, який це зробить:

    public static void RemoveAll<T>(this ICollection<T> source, 
                                    Func<T, bool> predicate)
    {
        if (source == null)
            throw new ArgumentNullException("source", "source is null.");

        if (predicate == null)
            throw new ArgumentNullException("predicate", "predicate is null.");

        source.Where(predicate).ToList().ForEach(e => source.Remove(e));
    }

Засновано на: http://phejndorf.wordpress.com/2011/03/09/a-removeall-extension-for-the-collection-class/


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

10

Для простої структури списку найефективнішим способом видається використання реалізації Predicate RemoveAll.

Напр.

 workSpace.RoleAssignments.RemoveAll(x =>x.Member.Name == shortName);

Причини:

  1. Метод Predicate / Linq RemoveAll реалізований у списку та має доступ до внутрішнього масиву, що зберігає фактичні дані. Це змістить дані та змінить розмір внутрішнього масиву.
  2. Реалізація методу RemoveAt відбувається досить повільно і копіює весь базовий масив даних у новий масив. Це означає, що зворотна ітерація марна для Списку

Якщо ви застрягли впровадити це в епоху до c # 3.0. У вас є 2 варіанти.

  • Легко підтримуваний варіант. Скопіюйте всі відповідні елементи в новий список і поміняйте місцями основний список.

Напр.

List<int> list2 = new List<int>() ; 
foreach (int i in GetList())
{
    if (!(i % 2 == 0))
    {
        list2.Add(i);
    }
}
list2 = list2;

Або

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

Якщо ви дійсно часто видаляєте речі зі списку, можливо, для цієї мети краще підійде інша структура, така як HashTable (.net 1.1) або Словник (.net 2.0) або HashSet (.net 3.5).


7

Який тип колекції? Якщо це список, ви можете скористатися корисним "RemoveAll":

int cnt = workspace.RoleAssignments
                      .RemoveAll(spa => spa.Member.Name == shortName)

(Це працює в .NET 2.0. Звичайно, якщо у вас немає нового компілятора, вам доведеться використовувати "делегат (SPRoleAssignment spa) {return spa.Member.Name == shortName;}" замість nice синтаксис лямбда.)

Інший підхід, якщо це не Список, а все ж ICollection:

   var toRemove = workspace.RoleAssignments
                              .FirstOrDefault(spa => spa.Member.Name == shortName)
   if (toRemove != null) workspace.RoleAssignments.Remove(toRemove);

Для цього потрібні методи розширення Enumerable. (Ви можете скопіювати монофонічні файли, якщо ви застрягли на .NET 2.0). Якщо це якась спеціальна колекція, яка не може взяти предмет, але ПОВИННА взяти індекс, деякі інші методи Enumerable, такі як Select, передають цілий індекс для вас.


2

Це моє загальне рішення

public static IEnumerable<T> Remove<T>(this IEnumerable<T> items, Func<T, bool> match)
    {
        var list = items.ToList();
        for (int idx = 0; idx < list.Count(); idx++)
        {
            if (match(list[idx]))
            {
                list.RemoveAt(idx);
                idx--; // the list is 1 item shorter
            }
        }
        return list.AsEnumerable();
    }

Виглядало б набагато простіше, якби методи розширення підтримували передачу за посиланням! використання:

var result = string[]{"mike", "john", "ali"}
result = result.Remove(x => x.Username == "mike").ToArray();
Assert.IsTrue(result.Length == 2);

РЕДАГУВАТИ: забезпечено, щоб цикл списку залишався чинним навіть під час видалення елементів шляхом зменшення індексу (idx).


Це не робоче рішення! Ви пропускаєте елементи як в кінці, так і в середині списку! Спробуйте, наприклад var result = string[]{"mike", "mike", "john", "ali", "mike"}, буде видалено лише перший "мікрофон" .
flindeberg

@flindeberg Цікаво, що ви маєте на увазі під "пропуском", оскільки цикл проходить над усіма елементами?! ... але я спробую ваші дані
Джалал Ель-Шаер

Коли ви збільшуєте свій індекс (idx) після видалення елемента, ви пропускаєте елемент "наступний". Приклад: Якщо ви видалите елемент у положенні 4 , положення 5 буде "переміщено" у положення 4 , положення 6 -> положення 5 , (n) -> (n - 1) тощо, а потім ви збільшите свій індекс до 5, де ви знайдете (старий 6, новий 5) , і ви пропустили (старий 5, новий 4) . Це пояснює речі?
flindeberg

Дуже добре, я оновив свою відповідь ... дякую, що помітили це;)
Джалал Ель-Шаер

2

Ось досить хороший спосіб це зробити

http://support.microsoft.com/kb/555972

        System.Collections.ArrayList arr = new System.Collections.ArrayList();
        arr.Add("1");
        arr.Add("2");
        arr.Add("3");

        /*This throws an exception
        foreach (string s in arr)
        {
            arr.Remove(s);
        }
        */

        //where as this works correctly
        Console.WriteLine(arr.Count);
        foreach (string s in new System.Collections.ArrayList(arr)) 
        {
            arr.Remove(s);
        }
        Console.WriteLine(arr.Count);
        Console.ReadKey();

0

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

коротке ім'я => SPRoleAssignment

Якщо ви зробите це, тоді, коли ви хочете видалити елемент із коротким ім'ям, все, що вам потрібно зробити, це видалити елемент з хеш-таблиці за допомогою ключа.

На жаль, якщо ви багато завантажуєте ці SPRoleAssignments, це, очевидно, не буде більш економічно ефективним з точки зору часу. Пропозиції інших людей щодо використання Linq були б непоганими, якщо ви використовуєте нову версію .NET Framework, але в іншому випадку вам доведеться дотримуватися методу, який ви використовуєте.


0

Подібно до точки зору Збірника словників, я зробив це.

Dictionary<string, bool> sourceDict = new Dictionary<string, bool>();
sourceDict.Add("Sai", true);
sourceDict.Add("Sri", false);
sourceDict.Add("SaiSri", true);
sourceDict.Add("SaiSriMahi", true);

var itemsToDelete = sourceDict.Where(DictItem => DictItem.Value == false);

foreach (var item in itemsToDelete)
{
    sourceDict.Remove(item.Key);
}

Примітка: Вищезазначений код не вдасться отримати в .Net Client Profile (3.5 та 4.5). Також деякі глядачі згадали, що він не працює в .Net4.0, а також не впевнені, які налаштування викликають проблему.

Тож замініть на код нижче .ToList () для оператора Where, щоб уникнути цієї помилки. «Колекція була змінена; операція перерахування може не виконуватися. "

var itemsToDelete = sourceDict.Where(DictItem => DictItem.Value == false).ToList();

За MSDN, починаючи з .Net4.5, профіль клієнта припиняється. http://msdn.microsoft.com/en-us/library/cc656912(v=vs.110).aspx


0

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

var itemsToDelete = Items.Where(x => !!!your condition!!!).ToArray();
for (int i = 0; i < itemsToDelete.Length; ++i)
    Items.Remove(itemsToDelete[i]);

Вам потрібно замінити GetHashCode()у своєму класі Item.


0

Найкращий спосіб це зробити, використовуючи linq.

Приклад класу:

 public class Product
    {
        public string Name { get; set; }
        public string Price { get; set; }      
    }

Запит Linq:

var subCollection = collection1.RemoveAll(w => collection2.Any(q => q.Name == w.Name));

Цей запит видалить усі елементи, collection1якщо вони Nameвідповідають будь-яким елементам Nameзcollection2

Не забувайте використовувати: using System.Linq;


0

Щоб зробити це, переглядаючи колекцію, а не отримуючи модифікацію винятку колекції, такий підхід я застосовував раніше (зверніть увагу на .ToList () в кінці оригінальної колекції, це створює іншу колекцію в пам'яті , тоді ви можете змінити існуючу колекцію)

foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments.ToList())
{
    if (spAssignment.Member.Name == shortName)
    {
        workspace.RoleAssignments.Remove(spAssignment);
    }
}

0

Якщо у вас є List<T>, тоді List<T>.RemoveAllце найкращий вибір. Не може бути нічого ефективнішого. Внутрішньо це робить масив, що рухається одним пострілом, не кажучи вже про те, що це O (N).

Якщо все, що у вас є, це IList<T>або приблизно, ICollection<T>ви отримали приблизно ці три варіанти:

    public static void RemoveAll<T>(this IList<T> ilist, Predicate<T> predicate) // O(N^2)
    {
        for (var index = ilist.Count - 1; index >= 0; index--)
        {
            var item = ilist[index];
            if (predicate(item))
            {
                ilist.RemoveAt(index);
            }
        }
    }

або

    public static void RemoveAll<T>(this ICollection<T> icollection, Predicate<T> predicate) // O(N)
    {
        var nonMatchingItems = new List<T>();

        // Move all the items that do not match to another collection.
        foreach (var item in icollection) 
        {
            if (!predicate(item))
            {
                nonMatchingItems.Add(item);
            }
        }

        // Clear the collection and then copy back the non-matched items.
        icollection.Clear();
        foreach (var item in nonMatchingItems)
        {
            icollection.Add(item);
        }
    }

або

    public static void RemoveAll<T>(this ICollection<T> icollection, Func<T, bool> predicate) // O(N^2)
    {
        foreach (var item in icollection.Where(predicate).ToList())
        {
            icollection.Remove(item);
        }
    }

Виберіть або 1, або 2.

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

2 швидше, якщо у вас є більше видалень.

3 - найчистіший код, але погано працює з IMO. Знову ж все, що залежить від вхідних даних.

Деякі деталі порівняльного порівняння див. Https://github.com/dotnet/BenchmarkDotNet/issues/1505


0

Тут багато хороших відповідей; Мені особливо подобаються лямбда-вирази ... дуже чисті. Однак я був знедолений, не вказавши тип колекції. Це SPRoleAssignmentCollection (від MOSS), який має лише Remove (int) та Remove (SPPrincipal), а не зручний RemoveAll (). Отже, я зупинився на цьому, якщо немає кращої пропозиції.

foreach (SPRoleAssignment spAssignment in workspace.RoleAssignments)
{
    if (spAssignment.Member.Name != shortName) continue;
    workspace.RoleAssignments.Remove((SPPrincipal)spAssignment.Member);
    break;
}

2
Вам все одно краще з моєю іншою пропозицією використовувати FirstOrDefault. things.Remove (things.FirstOrDefault (t => t.Whatever == true))
MichaelGG
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.