Чому словник не має AddRange?


115

Заголовок досить базовий, чому я не можу:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());


2
Є багато таких, що не мають AddRange, що завжди мене містифікувало. Як і колекція <>. Просто завжди здавалося дивним, що у Списку <> він був, але не Collection <> або інші об'єкти IList та ICollection.
Тім

39
Я збираюся витягнути сюди Еріка Ліпперта: "тому що ніхто ніколи не розробляв, не уточнював, не реалізував, не перевіряв, не підтверджував, не задокументував та не передав цю функцію".
Гейб Мутхарт

3
@ Gabe Moothart - саме так я припускав. Я люблю використовувати цю лінію на інших людях. Вони ненавиджу це. :)
Тім

2
@GabeMoothart чи не було б простіше сказати "Тому що ти не можеш" або "Тому що це не так" або навіть "Тому що"? - Я здогадуюсь, це не так весело сказати чи щось таке? --- Моє наступне запитання до вашої (я думаю, цитованої чи перефразованої) відповіді - "Чому ніхто ніколи не проектував, не вказував, не втілював, не тестував, не документував і не надсилав цю функцію?", До якого ви могли б бути змушений відповісти "Тому що ніхто не робив", що нарівні з відповідями, які я запропонував раніше. Єдиний інший варіант, який я можу передбачити, - це не саркастично, а це те, що в дійсності цікавилась ОП.
Code Jockey

Відповіді:


76

Коментар до оригінального питання підсумовує це досить добре:

тому що ніхто ніколи не розробляв, не уточнював, не реалізовував, не перевіряв, не задокументував та не постачав цю функцію - @Gabe Moothart

А чому? Ну, ймовірно, тому що поведінку об'єднаних словників не може бути аргументовано таким чином, що відповідає рамковим вказівкам.

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)
Гусман

19
Якщо він може мати Додати, він також повинен мати можливість додавати кілька. Поведінка, коли ви намагаєтеся додати елементи з повторюваними ключами, повинна бути такою ж, як і при додаванні єдиних повторюваних ключів.
Uriah Blatherwick

5
AddMultipleвідрізняється від того AddRange, незалежно від того, що його реалізація буде виграшною: ви кидаєте виняток із масиву всіх повторюваних ключів? Або ви кидаєте виняток на перший повторюваний ключ, який ви зустрічаєте? Яким має бути стан словника, якщо викинутий виняток? Пріштін, чи всі ключі, які вдалися?
Алан

3
Гаразд, тепер мені доведеться вручну повторити свої переліки та додати їх окремо, з усіма застереженнями про дублікати, які ви згадали. Яким чином його опускання з рамки щось вирішує?
doug65536

4
@ doug65536 Тому що ви, як споживач API, тепер можете вирішити, що ви хочете робити з кожною особою Add- або загорнувши кожного Addв, try...catchі таким чином ловити дублікати; або використовувати індексатор і перезаписати перше значення з пізнішим значенням; або попередньо перевірити використання, ContainsKeyперш ніж намагатися Add, зберігаючи початкове значення. Якби рамки мали AddRangeабо AddMultipleметод, єдиний простий спосіб донести те, що сталося, був би через виняток, а обробка та відновлення, пов'язане з цим, було б не менш складним.
Зев Шпіц

36

У мене є рішення:

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)...
Панайотис Канавос

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

Моя здогадка - відсутність належного висновку для користувача щодо того, що сталося. Оскільки ви не можете мати повторювані ключі у словниках, як би ви обробляли об'єднання двох словників, де деякі ключі перетинаються? Звичайно, ви можете сказати: "Мені все одно", але це порушує умову повернення помилок / викидання виключення для повторення ключів.


5
Чим це відрізняється від того, де у вас виникає зіткнення ключів під час дзвінка Add, окрім того, це може статися не один раз. Це кине те саме, ArgumentExceptionщо Addробить, напевно?
nicodemus13

1
@ nicodemus13 Так, але ви не знаєте, який ключ кинув Виняток, лише, що ДЕЯКІЙ ключ був повтором.
Гал

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

1
У словнику вже є конструктор, який приймає інший словник .
Панайотис Канавос

1
OP хоче доповнити, а не клонувати словник. Що стосується назви методу в моєму прикладі MethodThatReturnAnotherDic. Походить від ОП. Перегляньте питання і мою відповідь ще раз.
Валамас

1

Якщо ви маєте справу з новим словником (і у вас немає існуючих рядків для втрати), ви завжди можете використовувати ToDictionary () з іншого списку об'єктів.

Отже, у вашому випадку ви зробите щось подібне:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);

5
Вам навіть не потрібно створювати новий словник, просто напишітьDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos

1

Якщо ви знаєте, що у вас не буде дублікатів ключів, ви можете:

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

Це буде винятком, якщо є дублікат пари ключ / значення.

Я не знаю, чому це не в рамках; має бути. Невідомості немає; просто киньте виняток. У випадку з цим кодом він робить виняток.


Що робити, якщо оригінальний словник був створений за допомогою 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;
}

0

Ось альтернативне рішення, використовуючи c # 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>інтерфейс не робить з тих же причин: очікувана поведінка при додаванні діапазону значень до колекції може змінюватися в широких межах, залежно від контексту.

Контекст вашого запитання говорить про те, що ви не турбуєтесь про дублікати, і в цьому випадку у вас є проста альтернатива oneliner, використовуючи Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.