Почему в словаре нет AddRange?

115

Название достаточно простое, почему я не могу:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());
Custodio
источник
2
У многих нет AddRange, что меня всегда озадачивало. Нравится Коллекция <>. Просто всегда казалось странным, что он был у List <>, но не у Collection <> или других объектов IList и ICollection.
Тим
39
Я собираюсь привлечь сюда Эрика Липперта: «потому что никто никогда не проектировал, не специфицировал, не реализовывал, не тестировал, не документировал и не поставлял эту функцию».
Гейб Мутхарт
3
@ Гейб Мутхарт - это именно то, что я предполагал. Я люблю использовать эту фразу на других людях. Но они это ненавидят. :)
Тим
2
@GabeMoothart не было бы проще сказать «Потому что ты не можешь», «Потому что нет» или даже «Потому что»? - Полагаю, это не так весело говорить, что ли? --- Мой следующий вопрос к вашему (я предполагаю, цитировал или перефразировал) ответ: «Почему никто никогда не спроектировал, не указал, не реализовал, не протестировал, не задокументировал и не отправил эту функцию?», И вы вполне могли бы быть был вынужден ответить «Потому что никто не сделал», что соответствует ответам, которые я предлагал ранее. Единственный другой вариант, который я могу представить, не является саркастическим, и это то, что на самом деле интересовало OP.
Code Jockey

Ответы:

76

Комментарий к исходному вопросу довольно хорошо резюмирует:

потому что никто никогда не проектировал, не специфицировал, не реализовывал, не тестировал, не документировал и не поставлял эту функцию. - @Gabe Moothart

А почему? Что ж, вероятно, потому, что поведение слияния словарей не может быть рассмотрено таким образом, чтобы соответствовать рекомендациям Framework.

AddRangeне существует, потому что диапазон не имеет значения для ассоциативного контейнера, поскольку диапазон данных допускает повторяющиеся записи. Например, если у вас есть такая IEnumerable<KeyValuePair<K,T>>коллекция, это не защищает от дублирования записей.

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

Каким должно быть поведение метода при работе с дубликатом?

Я могу придумать как минимум три решения:

  1. генерировать исключение для первой записи, которая является дубликатом
  2. генерировать исключение, содержащее все повторяющиеся записи
  3. Игнорировать дубликаты

В каком состоянии должен находиться исходный словарь при возникновении исключения?

Addпочти всегда реализуется как атомарная операция: она завершается успешно и обновляет состояние коллекции или терпит неудачу, а состояние коллекции остается неизменным. Поскольку AddRangeможет произойти сбой из-за повторяющихся ошибок, способ сохранить его поведение согласованным с Addтем, чтобы также сделать его атомарным, выбрасывая исключение для любого дубликата и оставив состояние исходного словаря неизменным.

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

Тогда выбор сводится к следующему:

  1. Вызывает исключение со всеми дубликатами, оставляя исходный словарь в покое.
  2. Игнорируйте дубликаты и продолжайте.

Есть аргументы в пользу обоих вариантов использования. Для этого нужно добавить IgnoreDuplicatesк подписи флажок?

IgnoreDuplicatesФлаг (если установлен верно) также обеспечит большую скорость вверх, в качестве базовой реализации обойдет код для дубликата проверки.

Итак, теперь у вас есть флаг, который позволяет AddRangeподдерживать оба случая, но имеет недокументированный побочный эффект (чего разработчики Framework очень старались избежать).

Резюме

Поскольку нет четкого, последовательного и ожидаемого поведения, когда дело доходит до работы с дубликатами, легче не обрабатывать их все вместе и не предоставлять метод для начала.

Если вам постоянно приходится объединять словари, вы, конечно, можете написать свой собственный метод расширения для объединения словарей, который будет вести себя так, как это работает для вашего приложения (приложений).

Алан
источник
37
Совершенно неверно, словарь должен иметь AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman
19
Если он может иметь Добавить, он также должен иметь возможность добавлять несколько. Поведение при попытке добавить элементы с повторяющимися ключами должно быть таким же, как при добавлении одного повторяющегося ключа.
Uriah Blatherwick 08
5
AddMultipleотличается от этого AddRange, независимо от того, что его реализация будет нестабильной: вы генерируете исключение с массивом всех повторяющихся ключей? Или вы создаете исключение для первого встречающегося дубликата ключа? Каким должно быть состояние словаря при возникновении исключения? Безупречный или все ключи, которые удались?
Алан
3
Хорошо, теперь мне нужно вручную перебирать свои перечислимые и добавлять их по отдельности, со всеми оговорками о дубликатах, которые вы упомянули. Как исключение этого из фреймворка что-нибудь решает?
doug65536 08
5
@ doug65536 Потому что вы, как потребитель API, теперь можете решить, что вы хотите делать с каждым отдельным пользователем Add- либо заключить каждый Addв a try...catchи таким образом перехватить дубликаты; или используйте индексатор и замените первое значение более поздним значением; или предварительно проверьте использование ContainsKeyперед попыткой Add, сохранив таким образом исходное значение. Если бы у фреймворка был метод AddRangeили AddMultiple, единственный простой способ сообщить о том, что произошло, - через исключение, а обработка и восстановление были бы не менее сложными.
Zev Spitz
37

У меня есть решение:

Dictionary<string, string> mainDic = new Dictionary<string, string>() { 
    { "Key1", "Value1" },
    { "Key2", "Value2.1" },
};
Dictionary<string, string> additionalDic= new Dictionary<string, string>() { 
    { "Key2", "Value2.2" },
    { "Key3", "Value3" },
};
mainDic.AddRangeOverride(additionalDic); // Overrides all existing keys
// or
mainDic.AddRangeNewOnly(additionalDic); // Adds new keys only
// or
mainDic.AddRange(additionalDic); // Throws an error if keys already exist
// or
if (!mainDic.ContainsKeys(additionalDic.Keys)) // Checks if keys don't exist
{
    mainDic.AddRange(additionalDic);
}

...

namespace MyProject.Helper
{
  public static class CollectionHelper
  {
    public static void AddRangeOverride<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic[x.Key] = x.Value);
    }

    public static void AddRangeNewOnly<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => { if (!dic.ContainsKey(x.Key)) dic.Add(x.Key, x.Value); });
    }

    public static void AddRange<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic.Add(x.Key, x.Value));
    }

    public static bool ContainsKeys<TKey, TValue>(this IDictionary<TKey, TValue> dic, IEnumerable<TKey> keys)
    {
        bool result = false;
        keys.ForEachOrBreak((x) => { result = dic.ContainsKey(x); return result; });
        return result;
    }

    public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        foreach (var item in source)
            action(item);
    }

    public static void ForEachOrBreak<T>(this IEnumerable<T> source, Func<T, bool> func)
    {
        foreach (var item in source)
        {
            bool result = func(item);
            if (result) break;
        }
    }
  }
}

Радоваться, веселиться.

ПРИЗНАВАТЬ
источник
Вам не нужно ToList(), словарь - это IEnumerable<KeyValuePair<TKey,TValue>. Кроме того, второй и третий методы будут выбрасываться, если вы добавите существующее значение ключа. Плохая идея, вы искали TryAdd? Наконец, второй может быть заменен наWhere(pair->!dic.ContainsKey(pair.Key)...
Панайотис Канавос 09
2
Хорошо, ToList()это не лучшее решение, поэтому я изменил код. Вы можете использовать, try { mainDic.AddRange(addDic); } catch { do something }если не уверены в третьем методе. Второй способ работает отлично.
ADM-IT
Привет, Панайотис Канавос, надеюсь, ты счастлив.
ADM-IT
1
Спасибо, я украл это.
Metabuddy
14

Если кто-то, как и я, сталкивается с этим вопросом - «AddRange» можно достичь, используя методы расширения IEnumerable:

var combined =
    dict1.Union(dict2)
        .GroupBy(kvp => kvp.Key)
        .Select(grp => grp.First())
        .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

Основная хитрость при объединении словарей - это дублирование ключей. В приведенном выше коде это часть .Select(grp => grp.First()). В этом случае он просто берет первый элемент из группы дубликатов, но при необходимости вы можете реализовать более сложную логику.

Рафаль Заяц
источник
Что делать, если dict1 не используется компаратор проверки на равенство по умолчанию?
mjwills
Методы Linq позволяют передавать IEqualityComparer, когда это необходимо:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Кайл МакКлеллан,
12

Я предполагаю, что пользователю не хватает информации о том, что произошло. Поскольку у вас не может быть повторяющихся ключей в словарях, как бы вы справились с объединением двух словарей, где некоторые ключи пересекаются? Конечно, вы могли бы сказать: «Мне все равно», но это нарушает соглашение о возврате false / выдаче исключения для повторяющихся ключей.

гал
источник
5
Чем это отличается от того, где у вас есть конфликт клавиш при звонке Add, кроме того, это может происходить более одного раза. Он бы бросил то же, ArgumentExceptionчто Addделает, неужели?
nicodemus13,
1
@ nicodemus13 Да, но вы не узнаете, какой ключ вызвал исключение, только тот ключ НЕКОТОРЫЙ был повторением.
Gal
1
@Gal предоставлен, но вы можете: вернуть имя конфликтующего ключа в сообщении об исключении (полезно для тех, кто знает, что они делают, я полагаю ...), ИЛИ вы могли бы указать его как (часть?) аргумент paramName к исключению ArgumentException, которое вы выбрасываете, ИЛИ-ИЛИ, вы можете создать новый тип исключения (возможно, достаточно универсальным вариантом может быть NamedElementException??), либо брошенным вместо, либо как внутреннее исключение исключения ArgumentException, которое указывает именованный элемент, который конфликтует ... несколько разных вариантов, я бы сказал
Code Jockey
7

Ты мог бы сделать это

Dictionary<string, string> dic = new Dictionary<string, string>();
// dictionary other items already added.
MethodThatReturnAnotherDic(dic);

public void MethodThatReturnAnotherDic(Dictionary<string, string> dic)
{
    dic.Add(.., ..);
}

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

List<KeyValuePair<string, string>>
Valamas
источник
1
В словаре уже есть конструктор, который принимает другой словарь .
Панайотис Канавос
1
OP хочет добавить диапазон, а не клонировать словарь. Что касается названия метода в моем примере MethodThatReturnAnotherDic. Это происходит из ОП. Пожалуйста, еще раз просмотрите вопрос и мой ответ.
Valamas
1

Если вы имеете дело с новым словарем (и у вас нет существующих строк, которые можно потерять), вы всегда можете использовать ToDictionary () из другого списка объектов.

Итак, в вашем случае вы бы сделали что-то вроде этого:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);
WEFX
источник
5
Вам даже не нужно создавать новый словарь, просто напишитеDictionary<string, string> dic = SomeList.ToDictionary...
Панайотис Канавос
1

Если вы знаете, что у вас не будет дублирующихся ключей, вы можете:

dic = dic.Union(MethodThatReturnAnotherDic()).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

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

Я не знаю, почему этого нет в рамках; должно быть. Нет никакой неопределенности; просто выбросьте исключение. В случае с этим кодом возникает исключение.

toddmo
источник
Что, если исходный словарь был создан с использованием var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills
1

Не стесняйтесь использовать такой метод расширения:

public static Dictionary<T, U> AddRange<T, U>(this Dictionary<T, U> destination, Dictionary<T, U> source)
{
  if (destination == null) destination = new Dictionary<T, U>();
  foreach (var e in source)
    destination.Add(e.Key, e.Value);
  return destination;
}
Franziee
источник
0

Вот альтернативное решение с использованием С # 7 ValueTuples (литералы кортежей)

public static class DictionaryExtensions
{
    public static Dictionary<TKey, TValue> AddRange<TKey, TValue>(this Dictionary<TKey, TValue> source,  IEnumerable<ValueTuple<TKey, TValue>> kvps)
    {
        foreach (var kvp in kvps)
            source.Add(kvp.Item1, kvp.Item2);

        return source;
    }

    public static void AddTo<TKey, TValue>(this IEnumerable<ValueTuple<TKey, TValue>> source, Dictionary<TKey, TValue> target)
    {
        target.AddRange(source);
    }
}

Используется как

segments
    .Zip(values, (s, v) => (s.AsSpan().StartsWith("{") ? s.Trim('{', '}') : null, v))
    .Where(zip => zip.Item1 != null)
    .AddTo(queryParams);
Андерс
источник
0

Как уже упоминали другие, причина, по которой Dictionary<TKey,TVal>.AddRangeне реализовано, заключается в том, что существуют различные способы обработки случаев, когда у вас есть дубликаты. Это также касается Collectionили интерфейсов , таких как IDictionary<TKey,TVal>, ICollection<T>и т.д.

Только List<T>реализует его, и вы заметите, что IList<T>интерфейс этого не делает по тем же причинам: ожидаемый поведение при добавлении диапазона значений в коллекцию может широко варьироваться в зависимости от контекста.

Контекст вашего вопроса предполагает, что вы не беспокоитесь о дубликатах, и в этом случае у вас есть простая альтернатива oneeliner с использованием Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Ama
источник