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

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 не нашел .

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

TrueWill
источник
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);
        }
    }
}
rymdsmurf
источник
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помощью набора будет установлена ​​емкость, а затем добавлены элементы (быстрее). Этот метод расширения будет очень медленным, но будет работать.

jvitor83
источник
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;
        }
    }
MovGP0
источник
Ответ 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);
Катарина Келам
источник