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

Каков наилучший способ подхода к удалению элементов из коллекции на С#, как только элемент известен, но не индекс. Это один из способов сделать это, но в лучшем случае он кажется неэлегантным.

//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 дополнительных переменных.

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

@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);
}

Ответ 4

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

Eg.

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

Причины:

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

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

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

Eg.

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) лучше подходят для этой цели.

Ответ 5

Если это 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/

Ответ 6

Какой тип коллекции? Если это список, вы можете использовать полезный "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 extension. (Вы можете скопировать Mono, если вы застряли на .NET 2.0). Если какая-то пользовательская коллекция, которая не может взять элемент, но ДОЛЖНА принять индекс, некоторые из других методов Enumerable, таких как Select, передают вам целочисленный индекс.

Ответ 7

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

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

Ответ 8

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

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

EDIT: убедитесь, что цикл списка остается в силе даже при удалении элементов путем уменьшения индекса (idx).

Ответ 9

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

shortname = > SPRoleAssignment

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

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

Ответ 10

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

Ответ 11

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

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

Ответ 12

Как и в словарной коллекции, я сделал это.

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

Ответ 13

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

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

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

Ответ 14

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