Dlaczego słownik nie ma AddRange?


115

Tytuł jest wystarczająco prosty, dlaczego nie mogę:

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


2
Jest wiele rzeczy, które nie mają AddRange, co zawsze mnie zadziwiało. Podobnie jak kolekcja <>. Po prostu zawsze wydawało się dziwne, że List <> go ma, ale nie Collection <> lub inne obiekty IList i ICollection.
Tim

39
Wyciągnę tutaj Eric Lippert: „ponieważ nikt nigdy nie zaprojektował, nie wyszczególnił, nie zaimplementował, nie przetestował, nie udokumentował ani nie wysłał tej funkcji”.
Gabe Moothart

3
@ Gabe Moothart - dokładnie to założyłem. Uwielbiam używać tej linii na innych ludziach. Ale oni tego nienawidzą. :)
Tim

2
@GabeMoothart czy nie byłoby prościej powiedzieć „Ponieważ nie możesz” lub „Ponieważ tak nie jest” lub nawet „Ponieważ”? - Zgaduję, że to nie jest tak zabawne, czy coś? --- Moje pytanie uzupełniające do Twojej (chyba zacytowanej lub sparafrazowanej) odpowiedzi brzmi: „Dlaczego nikt nigdy nie zaprojektował, nie określił, nie wdrożył, nie przetestował, nie udokumentował i nie wysłał tej funkcji?”, Do którego możesz być bardzo dobrze zmuszony do odpowiedzi „Ponieważ nikt tego nie zrobił”, co jest zgodne z odpowiedziami, które zasugerowałem wcześniej. Jedyna inna opcja, jaką mogę sobie wyobrazić, nie jest sarkastyczna i właśnie nad tym zastanawiał się OP.
Code Jockey

Odpowiedzi:


76

Komentarz do pierwotnego pytania podsumowuje to całkiem dobrze:

ponieważ nikt nigdy nie zaprojektował, nie wyszczególnił, nie wdrożył, nie przetestował, nie udokumentował ani nie dostarczył tej funkcji. - @Gabe Moothart

A dlaczego? Cóż, prawdopodobnie dlatego, że nie można uzasadnić zachowania scalania słowników w sposób zgodny z wytycznymi Framework.

AddRangenie istnieje, ponieważ zakres nie ma żadnego znaczenia dla kontenera asocjacyjnego, ponieważ zakres danych pozwala na zduplikowane wpisy. Np. Jeśli masz taką IEnumerable<KeyValuePair<K,T>>kolekcję, nie chroni przed zduplikowanymi wpisami.

Zachowanie polegające na dodaniu kolekcji par klucz-wartość lub nawet scaleniu dwóch słowników jest proste. Jednak nie jest to sposób postępowania z wieloma zduplikowanymi wpisami.

Jakie powinno być zachowanie metody w przypadku duplikatu?

Są co najmniej trzy rozwiązania, które przychodzą mi do głowy:

  1. zgłoś wyjątek dla pierwszego wpisu, który jest duplikatem
  2. zgłoś wyjątek zawierający wszystkie zduplikowane wpisy
  3. Ignoruj ​​duplikaty

Jaki powinien być stan oryginalnego słownika po zgłoszeniu wyjątku?

Addjest prawie zawsze implementowana jako operacja atomowa: kończy się powodzeniem i aktualizuje stan kolekcji lub kończy się niepowodzeniem, a stan kolekcji pozostaje niezmieniony. Ponieważ AddRangemoże się nie powieść z powodu zduplikowanych błędów, sposobem zachowania spójności z jego zachowaniem Addbyłoby również uczynienie go niepodzielnym przez rzucenie wyjątku na dowolny duplikat i pozostawienie stanu oryginalnego słownika bez zmian.

Jako konsument interfejsu API byłoby żmudne iteracyjne usuwanie zduplikowanych elementów, co oznacza, że AddRangepowinien zgłosić pojedynczy wyjątek zawierający wszystkie zduplikowane wartości.

Wybór sprowadza się wówczas do:

  1. Wrzuć wyjątek do wszystkich duplikatów, pozostawiając oryginalny słownik w spokoju.
  2. Zignoruj ​​duplikaty i kontynuuj.

Istnieją argumenty za wsparciem obu przypadków użycia. Aby to zrobić, czy dodajesz IgnoreDuplicatesflagę do podpisu?

IgnoreDuplicatesFlag (gdy wartość true) będzie również zapewniać znaczną prędkość w górę, jako podstawowej implementacji ominie kodzie duplikatu kontroli.

Więc teraz masz flagę, która pozwala na AddRangeobsługę obu przypadków, ale ma nieudokumentowany efekt uboczny (nad którym projektanci Framework naprawdę ciężko pracowali).

Podsumowanie

Ponieważ nie ma jasnego, spójnego i oczekiwanego zachowania, jeśli chodzi o postępowanie z duplikatami, łatwiej jest nie zajmować się nimi wszystkimi razem i nie zapewnić metody na początek.

Jeśli ciągle będziesz musiał scalać słowniki, możesz oczywiście napisać własną metodę rozszerzenia, aby scalić słowniki, które będą zachowywać się w sposób odpowiedni dla Twojej aplikacji (-ów).


37
Całkowicie fałszywe, słownik powinien mieć AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman

19
Jeśli może mieć dodawanie, powinno również mieć możliwość dodawania wielu. Zachowanie podczas próby dodania elementów ze zduplikowanymi kluczami powinno być takie samo, jak podczas dodawania pojedynczych zduplikowanych kluczy.
Uriah Blatherwick

5
AddMultipleróżni się od AddRangetego, niezależnie od tego, czy jego implementacja będzie wątpliwa: czy rzucasz wyjątek z tablicą wszystkich zduplikowanych kluczy? A może rzucasz wyjątek na pierwszy napotkany duplikat klucza? Jaki powinien być stan słownika, jeśli zostanie zgłoszony wyjątek? Nieskazitelny, czy wszystkie klucze, którym się udało?
Alan,

3
W porządku, więc teraz muszę ręcznie powtórzyć moje wyliczalne i dodać je indywidualnie, ze wszystkimi zastrzeżeniami dotyczącymi duplikatów, o których wspomniałeś. Jak pominięcie tego we frameworku coś rozwiązuje?
doug65536

4
@ doug65536 Ponieważ Ty, jako konsument API, możesz teraz zdecydować, co chcesz zrobić z każdą osobą Add- albo zawiń każdą Addw a try...catchi złap w ten sposób duplikaty; lub użyj indeksatora i nadpisz pierwszą wartość późniejszą; lub zapobiegawczo sprawdź użycie ContainsKeyprzed próbą Add, zachowując w ten sposób oryginalną wartość. Gdyby struktura miała metodę AddRangelub AddMultiple, jedynym prostym sposobem przekazania informacji o tym, co się stało, byłby wyjątek, a obsługa i odzyskiwanie danych byłyby nie mniej skomplikowane.
Zev Spitz

36

Mam rozwiązanie:

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

Baw się dobrze.


Nie potrzebujesz ToList(), słownik to plik IEnumerable<KeyValuePair<TKey,TValue>. Ponadto druga i trzecia metoda metody zostanie wyświetlona, ​​jeśli dodasz istniejącą wartość klucza. Nie jest to dobry pomysł, szukałeś TryAdd? Ostatecznie drugiego można zastąpićWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos

2
Ok, ToList()to nie jest dobre rozwiązanie, więc zmieniłem kod. Możesz użyć, try { mainDic.AddRange(addDic); } catch { do something }jeśli nie jesteś pewien trzeciej metody. Druga metoda działa idealnie.
ADM-IT

Cześć Panagiotis Kanavos, mam nadzieję, że jesteś szczęśliwy.
ADM-IT

1
Dziękuję, ukradłem to.
metabuddy

14

W przypadku, gdy ktoś natknie się na to pytanie tak jak ja - możliwe jest osiągnięcie "AddRange" za pomocą metod rozszerzających IEnumerable:

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

Główną sztuczką podczas łączenia słowników jest radzenie sobie ze zduplikowanymi kluczami. W powyższym kodzie jest to część .Select(grp => grp.First()). W tym przypadku po prostu pobiera pierwszy element z grupy duplikatów, ale w razie potrzeby można tam zaimplementować bardziej wyrafinowaną logikę.


A co, jeśli dict1 nie używa domyślnej funkcji porównującej równość?
mjwills

Metody Linq pozwalają na przekazanie IEqualityComparer w odpowiednich przypadkach:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan

12

Domyślam się, że użytkownik nie ma odpowiednich informacji o tym, co się stało. Ponieważ w słownikach nie można mieć powtarzających się kluczy, jak poradziłbyś sobie z łączeniem dwóch słowników, w których krzyżują się niektóre klucze? Oczywiście można powiedzieć: „Nie obchodzi mnie to”, ale to łamie konwencję zwracania fałszu / wyrzucania wyjątku dla powtarzających się kluczy.


5
Czym różni się to od miejsca, w którym zderzasz się z klawiszami, gdy dzwonisz Add, poza tym, że może się to zdarzyć więcej niż raz. Rzuciłby to samo ArgumentException, Addco na pewno?
nicodemus13

1
@ nicodemus13 Tak, ale nie wiedziałbyś, który klawisz wywołał wyjątek, tylko że NIEKTÓRY klucz był powtórzeniem.
Gal

1
@Gal przyznane, ale możesz: zwrócić nazwę klucza powodującego konflikt w komunikacie o wyjątku (przydatne dla kogoś, kto wie, co robi, przypuszczam ...) LUB możesz umieścić go jako (część?) paramName argument do rzucanego ArgumentException, OR-OR, można utworzyć nowy typ wyjątku (być może wystarczającą opcją ogólną może być NamedElementException??), albo wyrzucony zamiast lub jako wewnętrzny wyjątek ArgumentException, który określa nazwany element, który powoduje konflikt ... kilka różnych opcji, powiedziałbym
Code Jockey

7

Mógłbyś to zrobić

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

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

lub użyj listy do dodawania i / lub używając powyższego wzorca.

List<KeyValuePair<string, string>>

1
Słownik ma już konstruktora, który akceptuje inny słownik .
Panagiotis Kanavos

1
OP chce dodać zakres, a nie klonować słownika. Jeśli chodzi o nazwę metody w moim przykładzie MethodThatReturnAnotherDic. Pochodzi z PO. Proszę ponownie przejrzeć pytanie i moją odpowiedź.
Valamas,

1

Jeśli masz do czynienia z nowym słownikiem (i nie masz istniejących wierszy do stracenia), zawsze możesz użyć ToDictionary () z innej listy obiektów.

Więc w twoim przypadku zrobiłbyś coś takiego:

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

5
Nie musisz nawet tworzyć nowego słownika, po prostu napiszDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos

1

Jeśli wiesz, że nie będziesz mieć zduplikowanych kluczy, możesz:

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

Zgłosi wyjątek, jeśli istnieje zduplikowana para klucz / wartość.

Nie wiem, dlaczego nie ma tego w ramach; Powinien być. Nie ma niepewności; po prostu wyrzuć wyjątek. W przypadku tego kodu zgłasza wyjątek.


A co, jeśli oryginalny słownik został utworzony przy użyciu var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills

1

Zapraszam do korzystania z takiej metody rozszerzenia:

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

Oto alternatywne rozwiązanie wykorzystujące C # 7 ValueTuples (literały krotki)

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

Używane jak

segments
    .Zip(values, (s, v) => (s.AsSpan().StartsWith("{") ? s.Trim('{', '}') : null, v))
    .Where(zip => zip.Item1 != null)
    .AddTo(queryParams);

0

Jak wspominali inni, powodem, dla którego Dictionary<TKey,TVal>.AddRangenie został zaimplementowany, jest to, że istnieje wiele sposobów obsługi przypadków, w których masz duplikaty. Jest to również przypadek Collectionlub interfejsy, takie jak IDictionary<TKey,TVal>, ICollection<T>itp

Tylko List<T>implementuje to, a zauważysz, że IList<T>interfejs nie działa z tych samych powodów: oczekiwane zachowanie podczas dodawania zakresu wartości do kolekcji może się znacznie różnić w zależności od kontekstu.

Kontekst twojego pytania sugeruje, że nie martwisz się o duplikaty, w takim przypadku masz prostą alternatywę oneliner, używając Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.