Лучший способ удалить предметы из коллекции

Как лучше всего подойти к удалению элементов из коллекции в 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 дополнительных переменных.

Любите имена переменных. Однако я бы не хотел быть тем @ss, который удаляют.

tvanfosson 16.10.2008 04:56

Добавьте оператор break при успешном поиске, если вы планируете это сделать, хотя использование словаря, вероятно, в любом случае лучше, если вы всегда ищите вещи по имени члена.

tvanfosson 16.10.2008 04:57

Я думаю, вы имели в виду RemoveAt () в своем фрагменте кода, поскольку вы передаете index. Как только элемент известен, вы можете напрямую вызвать Remove ().

hurst 16.10.2008 05:38

Этот вопрос следует прояснить. С какой структурой .Net имеют дело ответы? Мы говорим о List <T> или какой-либо другой структуре, реализующей IList <T> - ее, вероятно, следует переименовать в «Как лучше всего удалить элементы из List <T> в .net 3.0?»

Sam Saffron 16.10.2008 10:20
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
74
4
151 675
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

Ответ принят как подходящий

Если вы хотите получить доступ к членам коллекции по одному из их свойств, вы можете вместо этого использовать Dictionary<T> или KeyedCollection<T>. Таким образом, вам не нужно искать предмет, который вы ищете.

В противном случае вы могли бы хотя бы так:

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

Это вызовет исключение, потому что вы изменяете коллекцию по мере ее использования ...

RWendi 16.10.2008 09:03

Нет, это не так. Это потому, что после удаления предмета есть перерыв.

jfs 16.10.2008 09:29

Просто на случай, если кто-то прочитает это и применит к ситуации, когда вы удаляете несколько элементов, сохраните несколько индексов в массив и используйте отдельный цикл for, который возвращается назад через массив удаления для удаления элементов.

Robert C. Barth 17.10.2008 04:08

Или просто отфильтруйте, используя workspace.RoleAssignments = workspace.RoleAssignments.Where (x => x.Member.Name! = ShortName) .ToList () ;. Но я предпочитаю ответ Лиама, когда это возможно.

David 28.01.2020 23:11

Если RoleAssignments - это List<T>, вы можете использовать следующий код.

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

Я думаю, что только RemoveAll имеет параметр функции. По крайней мере, я не могу его собрать с помощью Remove.

MichaelGG 16.10.2008 05:06

Да, это RemoveAll. Я на самом деле потратил время, чтобы проверить это, убедился, что это RemoveAll, и все еще вставил в Remove. Жаль, что у stackoverflow нет встроенного компилятора :)

JaredPar 16.10.2008 05:09

Это создает новый список, если я не ошибаюсь.

Richard Nienaber 16.10.2008 09:03

AFAIK, это удаление на месте.

Jon Limjap 16.10.2008 10:34

Какой тип коллекции? Если это List, вы можете использовать полезный «RemoveAll»:

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

(Это работает в .NET 2.0. Конечно, если у вас нет более новой версии компилятора, вам придется использовать "delegate (SPRoleAssignment spa) {return spa.Member.Name == shortName;}" вместо красивого лямбда-синтаксис.)

Другой подход, если это не список, но все же ICollection:

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

Для этого требуются методы расширения Enumerable. (Вы можете скопировать Mono, если вы застряли на .NET 2.0). Если это какая-то настраиваемая коллекция, которая не может принимать элемент, но ДОЛЖНА принимать индекс, некоторые другие методы Enumerable, такие как Select, передают целочисленный индекс за вас.

Для простой структуры списка наиболее эффективным способом является использование реализации Predicate RemoveAll.

Например.

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

Причины:

  1. Метод Predicate / Linq RemoveAll реализован в List и имеет доступ к внутреннему массиву, хранящему фактические данные. Он сдвинет данные и изменит размер внутреннего массива.
  2. Реализация метода RemoveAt довольно медленная и будет копировать весь базовый массив данных в новый массив. Это означает, что обратная итерация бесполезна для List

Если вы застряли в реализации этого в эпоху до C# 3.0. У вас есть 2 варианта.

  • Легко обслуживаемый вариант. Скопируйте все совпадающие элементы в новый список и поменяйте местами базовый список.

Например.

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

Или же

  • Сложный, немного более быстрый вариант, который включает в себя смещение всех данных в списке вниз, когда они не совпадают, а затем изменение размера массива.

Если вы действительно часто удаляете что-то из списка, возможно, для этой цели лучше подходит другая структура, такая как Хеш-таблица (.net 1.1), Словарь (.net 2.0) или HashSet (.net 3.5).

@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, но list [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);
}

Для простого List <T>, если вам нужно удалить более 1 элемента, обратный цикл for ВСЕГДА является наиболее эффективным способом сделать это. Это, безусловно, более эффективно, чем копирование данных в список listToRemove. Готов поспорить, реализация Linq использует тот же трюк.

Sam Saffron 16.10.2008 08:57

@ sambo99 Я полностью согласен и попытаюсь расширить ваш ответ, объяснив, почему прямая итерация не всегда работает. Также я указываю, что при отсутствии дополнительной информации обратная итерация не более и не менее эффективна, если вы удаляете не более 1 записи. O (n) так же хорош, как и со списками.

Robert Paulson 16.10.2008 09:23

Ерп. Я исправляю это сейчас ... List <T> имеет очень неприятную реализацию RemoveAt, самый эффективный способ - это копирование нужных нам данных из списка

Sam Saffron 16.10.2008 09:26

Представьте, что вы хотите, чтобы ваши данные были удалены с левого края (скажем, вы хотите удалить большинство левых элементов, но вы не знаете, сколько). Очевидно, обратное решение - наихудший способ сделать это .... В вашем примере вместо WHOOPSIE! вы должны были написать list.RemoveAt (idx--); Alrigth?

please delete me 18.03.2010 04:35

@ maxima120 вы правы, но если вы прочитали то, что я сказал, я пытался понять иллюстрировать, почему обратная итерация может быть более эффективной самый в то время. Я не утверждаю, что это универсальное применение. В настоящее время я бы не стал перебирать себя и предпочел бы версию LINQ.

Robert Paulson 19.03.2010 01:21

На самом деле это не LINQ - это просто методы List.

NetMage 26.07.2019 02:47

Есть другой подход, который вы можете использовать в зависимости от того, как вы используете свою коллекцию. Если вы загружаете задания один раз (например, при запуске приложения), вы можете «на лету» преобразовать коллекцию в хеш-таблицу, где:

shortname => SPRoleAssignment

Если вы это сделаете, то когда вы захотите удалить элемент по короткому имени, все, что вам нужно сделать, это удалить элемент из хеш-таблицы по ключу.

К сожалению, если вы часто загружаете эти SPRoleAssignments, это, очевидно, не будет более экономичным с точки зрения времени. Предложения других людей об использовании Linq были бы хороши, если вы используете новую версию .NET Framework, но в противном случае вам придется придерживаться того метода, который вы используете.

Здесь много хороших отзывов; Мне особенно нравятся лямбда-выражения ... очень чистые. Однако я был упущен, не указав тип Коллекции. Это 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;
}

Вам все еще лучше с моим другим предложением использовать FirstOrDefault. things.Remove (things.FirstOrDefault (t => t.Whatever == true))

MichaelGG 21.10.2008 00:17

Вот довольно хороший способ сделать это

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

Это мое общее решение

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 21.05.2012 12:25

@flindeberg Интересно, что вы имеете в виду под «пропуском», поскольку цикл проходит по всем элементам ?! ... но я попробую ваши данные

Jalal El-Shaer 24.05.2012 10:38

Когда вы увеличиваете свой индекс (idx) после удаления элемента, вы пропускаете «следующий» элемент. Пример: если вы удалите элемент в позиция 4, позиция 5 будет «перемещен» в позиция 4, позиция 6-> позиция 5, (n) -> (n - 1) и т.д., а затем вы увеличите свой индекс до 5, где вы найдете (старый 6, новый 5), и вы пропустили (старые 5, новые 4). Это проясняет ситуацию?

flindeberg 25.05.2012 11:48

Хорошо, я обновил свой ответ ... спасибо, что заметили это;)

Jalal El-Shaer 28.05.2012 01:27

Чтобы сделать это во время цикла по коллекции и не получать исключение модификации коллекции, это подход, который я использовал в прошлом (обратите внимание на .ToList () в конце исходной коллекции, это создает другую коллекцию в памяти , то вы можете изменить существующую коллекцию)

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

Если это 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 20.11.2014 11:43

Так же, как и с точки зрения коллекции словарей, я сделал это.

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 (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

Сначала сохраните свои элементы, а затем удалите их.

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

Вам необходимо переопределить GetHashCode() в своем классе Item.

Лучший способ сделать это - использовать 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;

Если у вас есть 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 - самый чистый код, но ИМО работает плохо. Опять же, все зависит от исходных данных.

Для некоторых деталей тестирования см. https://github.com/dotnet/BenchmarkDotNet/issues/1505

Другие вопросы по теме