Добавить диапазон в коллекцию


109

Сегодня сотрудник спросил меня, как добавить диапазон в коллекцию. У него есть класс, наследующий от Collection<T>. У этого типа есть свойство только для получения, которое уже содержит некоторые элементы. Он хочет добавить элементы из другой коллекции в коллекцию свойств. Как он может сделать это в стиле C # 3? (Обратите внимание на ограничение свойства get-only, которое предотвращает такие решения, как выполнение Union и переназначение.)

Конечно, foreach с Property. Добавить будет работать. Но List<T>AddRange в стиле -стайл был бы намного элегантнее.

Достаточно просто написать метод расширения:

public static class CollectionHelpers
{
    public static void AddRange<T>(this ICollection<T> destination,
                                   IEnumerable<T> source)
    {
        foreach (T item in source)
        {
            destination.Add(item);
        }
    }
}

Но у меня такое чувство, что я изобретаю велосипед заново. Ничего подобного в System.Linqили morelinq не нашел .

Плохой дизайн? Просто позвоните и добавьте? Упускаете очевидное?


5
Помните, что Q из LINQ - это «запрос» и на самом деле касается извлечения данных, проецирования, преобразования и т. Д. Изменение существующих коллекций действительно не входит в сферу предполагаемой цели LINQ, поэтому LINQ ничего не дает. из коробки для этого. Но для этого идеально подойдут методы расширения (и в частности ваш образец).
Леви

Одна проблема, ICollection<T>вроде нет Addметода. msdn.microsoft.com/en-us/library/ ... Однако Collection<T>есть один.
Тим Гудман

@TimGoodman - это не общий интерфейс. См. Msdn.microsoft.com/en-us/library/92t2ye13.aspx
TrueWill

«Изменение существующих коллекций на самом деле не входит в сферу предполагаемой цели LINQ». @Levi Тогда почему вообще Add(T item)? Похоже на недоработанный подход, предлагающий возможность добавить один элемент, а затем ожидать, что все вызывающие абоненты будут выполнять итерацию, чтобы добавлять более одного элемента за раз. Ваше утверждение, безусловно, верно, IEnumerable<T>но я неоднократно разочаровывался в этом ICollections. Я не возражаю с вами, просто высказываюсь.
akousmata

Ответы:


62

Нет, это кажется вполне разумным. Существует метод List<T>.AddRange (), который в основном делает именно это, но требует, чтобы ваша коллекция была конкретной List<T>.


1
Спасибо; очень верно, но большинство общедоступных объектов следуют рекомендациям MS и не являются списками.
TrueWill

7
Да, я приводил это скорее как объяснение того, почему я не думаю, что это проблема. Просто поймите, что это будет менее эффективно, чем версия List <T> (поскольку list <T> может предварительно выделяться)
Рид Копси

Просто позаботьтесь о том, чтобы метод AddRange в .NET Core 2.2 мог показывать странное поведение при неправильном использовании, как показано в этой проблеме: github.com/dotnet/core/issues/2667
Бруно

36

Попробуйте выполнить приведение к списку в методе расширения перед запуском цикла. Таким образом, вы можете воспользоваться производительностью List.AddRange.

public static void AddRange<T>(this ICollection<T> destination,
                               IEnumerable<T> source)
{
    List<T> list = destination as List<T>;

    if (list != null)
    {
        list.AddRange(source);
    }
    else
    {
        foreach (T item in source)
        {
            destination.Add(item);
        }
    }
}

2
asОператор никогда не будет бросать. Если destinationне может быть приведен, listбудет равен нулю, и elseблок будет выполнен.
rymdsmurf

4
эргггх! Поменяйте местами ветки условий, ради всего святого!
nicodemus13

13
Вообще-то я серьезно. Основная причина в том, что это дополнительная когнитивная нагрузка, которая часто бывает действительно довольно сложной. Вы постоянно пытаетесь оценить отрицательные условия, что обычно относительно сложно, в любом случае у вас есть обе ветки, (ИМО) легче сказать, «if null» сделайте это, «else» сделайте это, а не наоборот. Речь также идет о значениях по умолчанию, они должны быть позитивными как можно чаще, например. `If (! Thing.IsDisabled) {} ​​else {} 'требует, чтобы вы остановились и подумали:« а, не отключено, значит включено, верно, получил это, значит, другая ветка отключена). Трудно разобрать.
nicodemus13

13
Интерпретировать «something! = Null» не сложнее, чем «something == null». Однако оператор отрицания - это совершенно другое дело, и в вашем последнем примере переписывание оператора if-else устранит этот оператор. Это объективное улучшение, но оно не связано с исходным вопросом. В этом конкретном случае две формы являются вопросом личных предпочтений, и я бы предпочел оператор "! =", Учитывая приведенные выше рассуждения.
rymdsmurf

15
Сопоставление с образцом сделает всех счастливыми ... ;-)if (destination is List<T> list)
Джейкоб Фоши 01

28

Поскольку, .NET4.5если вам нужен однострочник, вы можете использовать System.Collections.GenericForEach.

source.ForEach(o => destination.Add(o));

или даже короче как

source.ForEach(destination.Add);

По производительности он такой же, как и для каждого цикла (синтаксический сахар).

Также не пытайтесь назначить его как

var x = source.ForEach(destination.Add) 

причина ForEachнедействительна.

Изменить: скопировано из комментариев, мнение Липерта о ForEach


10
Лично я с Липпертом в этом вопросе
TrueWill

1
Должен ли он быть source.ForEach (destination.Add)?
Фрэнк

4
ForEachвроде бы определяется только на List<T>, а не Collection?
Protector

Теперь Липперта можно найти на web.archive.org/web/20190316010649/https://…
user7610

Обновлена ​​ссылка на сообщение в блоге Эрика Липперта: « Сказочные приключения в кодировании» | «Foreach» против «ForEach»
Александр

19

Помните, что каждый Addбудет проверять емкость коллекции и при необходимости изменять ее размер (медленнее). С AddRangeпомощью набора будет установлена ​​емкость, а затем добавлены элементы (быстрее). Этот метод расширения будет очень медленным, но будет работать.


3
Чтобы добавить к этому, также будет уведомление об изменении коллекции для каждого добавления, в отличие от одного массового уведомления с AddRange.
Ник Уделл

3

Вот немного более продвинутая / готовая к производству версия:

    public static class CollectionExtensions
    {
        public static TCol AddRange<TCol, TItem>(this TCol destination, IEnumerable<TItem> source)
            where TCol : ICollection<TItem>
        {
            if(destination == null) throw new ArgumentNullException(nameof(destination));
            if(source == null) throw new ArgumentNullException(nameof(source));

            // don't cast to IList to prevent recursion
            if (destination is List<TItem> list)
            {
                list.AddRange(source);
                return destination;
            }

            foreach (var item in source)
            {
                destination.Add(item);
            }

            return destination;
        }
    }

Ответ rymdsmurf может показаться наивным, слишком простым, но он работает с разнородными списками. Можно ли сделать так, чтобы этот код поддерживал этот вариант использования?
richardsonwtr,

Например: destinationэто список Shapeабстрактного класса. sourceэто список Circleунаследованного класса.
richardsonwtr,

1

Все классы библиотеки универсальных коллекций C5 поддерживают этот AddRangeметод. C5 имеет гораздо более надежный интерфейс , который на самом деле обнажает все особенности его основные реализаций и является интерфейсом , совместимым с System.Collections.Generic ICollectionи IListинтерфейсами, а это означает , чтоC5 коллекции «s может быть легко заменяемыми в качестве основной реализации.


0

Вы можете добавить свой диапазон IEnumerable в список, а затем установить ICollection = в список.

        IEnumerable<T> source;

        List<item> list = new List<item>();
        list.AddRange(source);

        ICollection<item> destination = list;

3
Функционально это работает, но нарушает правила Microsoft, согласно которым свойства коллекции должны быть доступны только для чтения ( msdn.microsoft.com/en-us/library/ms182327.aspx )
Ник Уделл,

0

Или вы можете просто сделать расширение ICollection следующим образом:

 public static ICollection<T> AddRange<T>(this ICollection<T> @this, IEnumerable<T> items)
    {
        foreach(var item in items)
        {
            @this.Add(item);
        }

        return @this;
    }

Его использование аналогично использованию в списке:

collectionA.AddRange(IEnumerable<object> items);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.