Perché Dictionary non ha AddRange?


115

Il titolo è abbastanza semplice, perché non posso:

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


2
Ci sono molti che non hanno AddRange, il che mi ha sempre sconcertato. Like Collection <>. Sembrava sempre strano che List <> lo avesse, ma non Collection <> o altri oggetti IList e ICollection.
Tim

39
Ho intenzione di tirare un Eric Lippert qui: "perché nessuno ha mai progettato, specificato, implementato, testato, documentato e distribuito quella funzione".
Gabe Moothart

3
@ Gabe Moothart - questo è esattamente quello che avevo pensato. Adoro usare quella linea su altre persone. Lo odiano però. :)
Tim

2
@GabeMoothart non sarebbe più semplice dire "Perché non puoi" o "Perché non lo fa" o anche "Perché"? - Immagino che non sia così divertente da dire o qualcosa del genere? --- La mia domanda di follow-up alla tua risposta (immagino citata o parafrasata) è "Perché nessuno ha mai progettato, specificato, implementato, testato, documentato e spedito quella funzione?", Alla quale potresti benissimo essere costretto a rispondere con "Perché nessuno l'ha fatto", che è alla pari con le risposte che ho suggerito prima. L'unica altra opzione che posso immaginare non è sarcastica ed è ciò che l'OP si stava effettivamente chiedendo.
Code Jockey

Risposte:


76

Un commento alla domanda originale lo riassume abbastanza bene:

perché nessuno ha mai progettato, specificato, implementato, testato, documentato e spedito quella funzione. - @Gabe Moothart

E perché? Bene, probabilmente perché il comportamento di unire i dizionari non può essere ragionato in un modo che si adatti alle linee guida del Framework.

AddRangenon esiste perché un intervallo non ha alcun significato per un contenitore associativo, poiché l'intervallo di dati consente voci duplicate. Ad esempio, se si IEnumerable<KeyValuePair<K,T>>dispone di una raccolta, non si proteggono le voci duplicate.

Il comportamento di aggiungere una raccolta di coppie chiave-valore o anche di unire due dizionari è semplice. Il comportamento di come gestire più voci duplicate, tuttavia, non lo è.

Quale dovrebbe essere il comportamento del metodo quando si tratta di un duplicato?

Ci sono almeno tre soluzioni a cui riesco a pensare:

  1. lancia un'eccezione per la prima voce che è un duplicato
  2. lancia un'eccezione che contiene tutte le voci duplicate
  3. Ignora i duplicati

Quando viene generata un'eccezione, quale dovrebbe essere lo stato del dizionario originale?

Addè quasi sempre implementato come operazione atomica: riesce e aggiorna lo stato della raccolta, oppure fallisce e lo stato della raccolta rimane invariato. Poiché AddRangepuò fallire a causa di errori duplicati, il modo per mantenere il suo comportamento coerente con Addsarebbe anche renderlo atomico lanciando un'eccezione su qualsiasi duplicato e lasciare invariato lo stato del dizionario originale.

Come consumatore di API, sarebbe noioso dover rimuovere in modo iterativo gli elementi duplicati, il che implica che AddRangedovrebbe generare una singola eccezione che contiene tutti i valori duplicati.

La scelta quindi si riduce a:

  1. Genera un'eccezione con tutti i duplicati, lasciando da solo il dizionario originale.
  2. Ignora i duplicati e procedi.

Ci sono argomenti per supportare entrambi i casi d'uso. Per farlo, aggiungi un IgnoreDuplicatesflag alla firma?

Il IgnoreDuplicatesflag (se impostato su true) fornirebbe anche una notevole velocità, poiché l'implementazione sottostante ignorerebbe il codice per il controllo dei duplicati.

Quindi ora, hai un flag che consente AddRangedi supportare entrambi i casi, ma ha un effetto collaterale non documentato (che è qualcosa che i progettisti del Framework hanno lavorato davvero duramente per evitare).

Sommario

Poiché non esiste un comportamento chiaro, coerente e previsto quando si tratta di gestire i duplicati, è più facile non affrontarli tutti insieme e non fornire il metodo con cui iniziare.

Se ti ritrovi a dover continuamente unire dizionari, puoi ovviamente scrivere il tuo metodo di estensione per unire i dizionari, che si comporterà in un modo che funziona per le tue applicazioni.


37
Totalmente falso, un dizionario dovrebbe avere un AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman

19
Se può avere Aggiungi, dovrebbe anche essere in grado di aggiungere più. Il comportamento quando si tenta di aggiungere elementi con chiavi duplicate dovrebbe essere lo stesso di quando si aggiunge una singola chiave duplicata.
Uriah Blatherwick

5
AddMultipleè diverso da AddRange, indipendentemente dalla sua implementazione sarà traballante: lanci un'eccezione con un array di tutte le chiavi duplicate? O lanci un'eccezione sulla prima chiave duplicata che incontri? Quale dovrebbe essere lo stato del dizionario se viene generata un'eccezione? Pristine, o tutte le chiavi che sono riuscite?
Alan

3
Ok, quindi ora devo iterare manualmente il mio enumerabile e aggiungerli individualmente, con tutti gli avvertimenti sui duplicati che hai menzionato. In che modo ometterlo dal framework risolve qualcosa?
doug65536

4
@ doug65536 Perché tu, come consumatore dell'API, ora puoi decidere cosa vuoi fare con ogni individuo Add: avvolgere ciascuno Addin una try...catche catturare i duplicati in questo modo; oppure utilizzare l'indicizzatore e sovrascrivere il primo valore con il valore successivo; o controllare preventivamente l'utilizzo ContainsKeyprima di tentare Add, preservando così il valore originale. Se il framework avesse un metodo AddRangeor AddMultiple, l'unico modo semplice per comunicare ciò che è accaduto sarebbe tramite un'eccezione, e la gestione e il recupero coinvolti non sarebbero meno complicati.
Zev Spitz

36

Ho qualche soluzione:

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

Divertiti.


Non hai bisogno ToList(), un dizionario è un file IEnumerable<KeyValuePair<TKey,TValue>. Inoltre, il secondo e il terzo metodo verranno generati se aggiungi un valore chiave esistente. Non è una buona idea, stavi cercando TryAdd? Infine, il secondo può essere sostituito conWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos

2
Ok, ToList()non è una buona soluzione, quindi ho cambiato il codice. Puoi usare try { mainDic.AddRange(addDic); } catch { do something }se non sei sicuro del terzo metodo. Il secondo metodo funziona perfettamente.
ADM-IT

Ciao Panagiotis Kanavos, spero che tu sia felice.
ADM-IT

1
Grazie, ho rubato questo.
metabuddy

14

Nel caso in cui qualcuno si imbatta in questa domanda come me, è possibile ottenere "AddRange" utilizzando i metodi di estensione IEnumerable:

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

Il trucco principale quando si combinano i dizionari è gestire le chiavi duplicate. Nel codice sopra è la parte .Select(grp => grp.First()). In questo caso si prende semplicemente il primo elemento dal gruppo di duplicati ma è possibile implementare una logica più sofisticata se necessario.


E se dict1 non si utilizza l'operatore di confronto di uguaglianza predefinito?
mjwills

I metodi Linq ti consentono di passare in IEqualityComparer quando pertinente:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan

12

La mia ipotesi è la mancanza di un output adeguato per l'utente su ciò che è accaduto. Dato che non puoi avere chiavi ripetute in un dizionario, come gestiresti l'unione di due dizionari dove alcune chiavi si intersecano? Certo potresti dire: "Non mi interessa" ma questo infrange la convenzione di restituire falso / lanciare un'eccezione per la ripetizione delle chiavi.


5
In cosa differisce da dove si verifica uno scontro di tasti quando si chiama Add, a parte questo può accadere più di una volta. Getterebbe lo stesso ArgumentExceptionche Addfa, sicuramente?
nicodemus13

1
@ nicodemus13 Sì, ma non sapresti quale chiave ha lanciato l'eccezione, solo che ALCUNA chiave era una ripetizione.
Gal

1
@Gal concesso, ma potresti: restituire il nome della chiave in conflitto nel messaggio di eccezione (utile a qualcuno che sa cosa stanno facendo, suppongo ...), O potresti metterlo come (parte di?) Il paramName dell'argomento ArgumentException che lanci, OR-OR, potresti creare un nuovo tipo di eccezione (forse un'opzione abbastanza generica potrebbe essere NamedElementException??), lanciata al posto o come innerException di un'ArgumentException, che specifica l'elemento denominato in conflitto ... alcune opzioni diverse, direi
Code Jockey

7

Potresti farlo

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

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

o usa un elenco per aggiungere intervallo e / o usando il modello sopra.

List<KeyValuePair<string, string>>

1
Dictionary ha già un costruttore che accetta un altro dizionario .
Panagiotis Kanavos

1
OP vuole aggiungere un intervallo, non clonare un dizionario. Per quanto riguarda il nome del metodo nel mio esempio MethodThatReturnAnotherDic. Viene dall'OP. Si prega di rivedere di nuovo la domanda e la mia risposta.
Valamas

1

Se hai a che fare con un nuovo dizionario (e non hai righe esistenti da perdere), puoi sempre utilizzare ToDictionary () da un altro elenco di oggetti.

Quindi, nel tuo caso, faresti qualcosa del genere:

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

5
Non è nemmeno necessario creare un nuovo dizionario, basta scrivereDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos

1

Se sai che non avrai chiavi duplicate, puoi fare:

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

Verrà generata un'eccezione se è presente una coppia chiave / valore duplicata.

Non so perché questo non sia nel quadro; dovrebbe essere. Non c'è incertezza; lancia solo un'eccezione. Nel caso di questo codice, genera un'eccezione.


E se il dizionario originale fosse stato creato usando var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills

1

Sentiti libero di usare un metodo di estensione come questo:

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

Ecco una soluzione alternativa usando c # 7 ValueTuples (tuple letterali)

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

Usato come

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

0

Come altri hanno già detto, il motivo per cui Dictionary<TKey,TVal>.AddRangenon è implementato è perché ci sono vari modi in cui potresti voler gestire i casi in cui hai duplicati. Questo è anche il caso Collectiono interfacce come IDictionary<TKey,TVal>, ICollection<T>ecc

Lo List<T>implementa solo e noterai che l' IList<T>interfaccia non lo fa, per gli stessi motivi: il previsto comportamento quando si aggiunge un intervallo di valori a una raccolta può variare ampiamente, a seconda del contesto.

Il contesto della tua domanda suggerisce che non sei preoccupato per i duplicati, nel qual caso hai una semplice alternativa oneliner, usando Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.