Pourquoi le dictionnaire n'a-t-il pas AddRange?

115

Le titre est assez basique, pourquoi ne puis-je pas:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic.AddRange(MethodThatReturnAnotherDic());
Custodio
la source
2
Il y en a beaucoup qui n'ont pas AddRange, ce qui m'a toujours mystifié. Comme Collection <>. Cela semblait toujours étrange que List <> l'ait, mais pas Collection <> ou d'autres objets IList et ICollection.
Tim
39
Je vais tirer un Eric Lippert ici: "parce que personne n'a jamais conçu, spécifié, implémenté, testé, documenté et expédié cette fonctionnalité."
Gabe Moothart
3
@ Gabe Moothart - c'est exactement ce que j'avais supposé. J'adore utiliser cette ligne sur d'autres personnes. Mais ils détestent ça. :)
Tim
2
@GabeMoothart ne serait-il pas plus simple de dire "Parce que vous ne pouvez pas" ou "Parce que ce n'est pas le cas" ou même "Parce que"? - Je suppose que ce n'est pas aussi amusant à dire ou quelque chose? --- Ma question de suivi à votre réponse (je suppose citée ou paraphrasée) est "Pourquoi personne n'a jamais conçu, spécifié, implémenté, testé, documenté et expédié cette fonctionnalité?", À laquelle vous pourriez très bien être forcé de répondre par «Parce que personne ne l'a fait», ce qui est à égalité avec les réponses que j'ai suggérées plus tôt. La seule autre option que je puisse envisager n'est pas sarcastique et c'est ce que l'OP se demandait réellement.
Code Jockey

Réponses:

76

Un commentaire à la question originale résume assez bien cela:

car personne n'a jamais conçu, spécifié, implémenté, testé, documenté et expédié cette fonctionnalité. - @Gabe Moothart

Quant à savoir pourquoi? Eh bien, probablement parce que le comportement de la fusion de dictionnaires ne peut pas être raisonné d'une manière qui correspond aux directives du Framework.

AddRangen'existe pas car une plage n'a aucune signification pour un conteneur associatif, car la plage de données permet des entrées en double. Par exemple, si vous en aviez une, IEnumerable<KeyValuePair<K,T>>cette collection ne vous protège pas contre les entrées en double.

Le comportement consistant à ajouter une collection de paires clé-valeur, ou même à fusionner deux dictionnaires est simple. Le comportement de la façon de traiter plusieurs entrées en double, cependant, ne l'est pas.

Quel doit être le comportement de la méthode lorsqu'elle traite un doublon?

Il y a au moins trois solutions auxquelles je peux penser:

  1. lever une exception pour la première entrée qui est un doublon
  2. lancer une exception qui contient toutes les entrées en double
  3. Ignorer les doublons

Lorsqu'une exception est levée, quel devrait être l'état du dictionnaire d'origine?

Addest presque toujours implémentée comme une opération atomique: elle réussit et met à jour l'état de la collection, ou elle échoue, et l'état de la collection reste inchangé. Comme cela AddRangepeut échouer en raison d'erreurs dupliquées, la façon de garder son comportement cohérent Addserait de le rendre également atomique en lançant une exception sur tout doublon, et de laisser l'état du dictionnaire d'origine inchangé.

En tant que consommateur d'API, il serait fastidieux de devoir supprimer de manière itérative les éléments en double, ce qui implique que le AddRangedevrait lever une seule exception contenant toutes les valeurs en double.

Le choix se résume alors à:

  1. Lancez une exception avec tous les doublons, laissant le dictionnaire d'origine seul.
  2. Ignorez les doublons et continuez.

Il existe des arguments pour prendre en charge les deux cas d'utilisation. Pour ce faire, ajoutez-vous un IgnoreDuplicatesdrapeau à la signature?

L' IgnoreDuplicatesindicateur (lorsqu'il est défini sur true) fournirait également une accélération significative, car l'implémentation sous-jacente contournerait le code pour la vérification des doublons.

Alors maintenant, vous avez un indicateur qui permet à la AddRangede prendre en charge les deux cas, mais a un effet secondaire non documenté (ce que les concepteurs du Framework ont ​​travaillé très dur pour éviter).

Résumé

Comme il n'y a pas de comportement clair, cohérent et attendu lorsqu'il s'agit de traiter les doublons, il est plus facile de ne pas les traiter tous ensemble et de ne pas fournir la méthode pour commencer.

Si vous devez continuellement fusionner des dictionnaires, vous pouvez bien sûr écrire votre propre méthode d'extension pour fusionner des dictionnaires, qui se comportera d'une manière qui fonctionne pour vos applications.

Alan
la source
37
Totalement faux, un dictionnaire doit avoir un AddRange (IEnumerable <KeyValuePair <K, T >> Values)
Gusman
19
S'il peut avoir Add, il devrait également pouvoir ajouter plusieurs. Le comportement lorsque vous essayez d'ajouter des éléments avec des clés en double doit être le même que lorsque vous ajoutez une seule clé en double.
Uriah Blatherwick
5
AddMultipleest différent de AddRange, quelle que soit sa mise en œuvre va être bancale: lancez-vous une exception avec un tableau de toutes les clés en double? Ou lancez-vous une exception sur la première clé en double que vous rencontrez? Quel devrait être l'état du dictionnaire si une exception est levée? Pristine, ou toutes les clés qui ont réussi?
Alan
3
D'accord, je dois maintenant itérer manuellement mes énumérables et les ajouter individuellement, avec toutes les mises en garde concernant les doublons que vous avez mentionnés. Comment l'omettre du framework résout-il quelque chose?
doug65536
4
@ doug65536 Parce que vous, en tant que consommateur d'API, pouvez maintenant décider de ce que vous voulez faire avec chaque individu Add- soit envelopper chacun Adddans un try...catchet attraper les doublons de cette façon; ou utilisez l'indexeur et remplacez la première valeur par la dernière valeur; ou vérifiez de manière préventive en utilisant ContainsKeyavant d'essayer Add, préservant ainsi la valeur d'origine. Si le framework avait une méthode AddRangeou AddMultiple, le seul moyen simple de communiquer ce qui s'était passé serait via une exception, et la gestion et la récupération impliquées ne seraient pas moins compliquées.
Zev Spitz
36

J'ai une solution:

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

S'amuser.

ADMETTRE
la source
Vous n'avez pas besoin ToList(), un dictionnaire est un fichier IEnumerable<KeyValuePair<TKey,TValue>. En outre, les deuxième et troisième méthodes de méthode seront lancées si vous ajoutez une valeur de clé existante. Pas une bonne idée, tu cherchais TryAdd? Enfin, le deuxième peut être remplacé parWhere(pair->!dic.ContainsKey(pair.Key)...
Panagiotis Kanavos
2
Ok, ce ToList()n'est pas une bonne solution alors j'ai changé le code. Vous pouvez utiliser try { mainDic.AddRange(addDic); } catch { do something }si vous n'êtes pas sûr de la troisième méthode. La deuxième méthode fonctionne parfaitement.
ADM-IT
Salut Panagiotis Kanavos, j'espère que vous êtes heureux.
ADM-IT
1
Merci, j'ai volé ça.
metabuddy le
14

Dans le cas où quelqu'un rencontre cette question comme moi, il est possible d'atteindre "AddRange" en utilisant les méthodes d'extension IEnumerable:

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

L'astuce principale lors de la combinaison de dictionnaires consiste à gérer les clés en double. Dans le code ci-dessus, c'est la partie .Select(grp => grp.First()). Dans ce cas, il prend simplement le premier élément du groupe de doublons mais vous pouvez y implémenter une logique plus sophistiquée si nécessaire.

Rafal Zajac
la source
Et si dict1 n'utilise pas le comparateur d'égalité par défaut?
mjwills
Les méthodes Linq vous permettent de passer dans IEqualityComparer le cas échéant:var combined = dict1.Concat(dict2).GroupBy(kvp => kvp.Key, dict1.Comparer).ToDictionary(grp => grp.Key, grp=> grp.First(), dict1.Comparer);
Kyle McClellan
12

Ma conjecture est le manque de sortie appropriée à l'utilisateur quant à ce qui s'est passé. Comme vous ne pouvez pas avoir de clés répétitives dans un dictionnaire, comment géreriez-vous la fusion de deux dictionnaires où certaines touches se croisent? Bien sûr, vous pouvez dire: "Je m'en fous", mais cela enfreint la convention consistant à renvoyer false / à lancer une exception pour la répétition des clés.

Fille
la source
5
En quoi est-ce différent de l'endroit où vous avez un conflit de touches lorsque vous appelez Add, à part cela, cela peut se produire plus d'une fois. Il jetterait le même ArgumentExceptionqui Addfait, sûrement?
nicodemus13
1
@ nicodemus13 Oui, mais vous ne sauriez pas quelle touche a jeté l'exception, seulement que QUELQUE touche était une répétition.
Gal
1
@Gal accordé, mais vous pouvez: renvoyer le nom de la clé en conflit dans le message d'exception (utile à quelqu'un qui sait ce qu'il fait, je suppose ...), OU vous pouvez le mettre comme (une partie de?) Le paramName à l'argument ArgumentException que vous lancez, OR-OR, vous pouvez créer un nouveau type d'exception (peut-être qu'une option assez générique pourrait être NamedElementException??), soit levée à la place de soit en tant que innerException d'une ArgumentException, qui spécifie l'élément nommé qui est en conflit ... quelques options différentes, je dirais
Code Jockey
7

Tu pourrais faire ça

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

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

ou utilisez une liste pour ajouter une plage et / ou utiliser le modèle ci-dessus.

List<KeyValuePair<string, string>>
Valamas
la source
1
Le dictionnaire a déjà un constructeur qui accepte un autre dictionnaire .
Panagiotis Kanavos
1
OP veut ajouter une gamme, pas cloner un dictionnaire. Quant au nom de la méthode dans mon exemple MethodThatReturnAnotherDic. Cela vient de l'OP. Veuillez revoir la question et ma réponse.
Valamas
1

Si vous traitez avec un nouveau dictionnaire (et que vous n'avez pas de lignes existantes à perdre), vous pouvez toujours utiliser ToDictionary () à partir d'une autre liste d'objets.

Donc, dans votre cas, vous feriez quelque chose comme ceci:

Dictionary<string, string> dic = new Dictionary<string, string>();
dic = SomeList.ToDictionary(x => x.Attribute1, x => x.Attribute2);
WEFX
la source
5
Vous n'avez même pas besoin de créer un nouveau dictionnaire, il suffit d'écrireDictionary<string, string> dic = SomeList.ToDictionary...
Panagiotis Kanavos
1

Si vous savez que vous n'allez pas avoir de clés en double, vous pouvez faire:

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

Il lèvera une exception s'il y a une paire clé / valeur en double.

Je ne sais pas pourquoi ce n'est pas dans le cadre; devrait être. Il n'y a pas d'incertitude; jetez simplement une exception. Dans le cas de ce code, il lève une exception.

Toddmo
la source
Et si le dictionnaire original a été créé avec var caseInsensitiveDictionary = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase);?
mjwills
1

N'hésitez pas à utiliser une méthode d'extension comme celle-ci:

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
la source
0

Voici une solution alternative utilisant c # 7 ValueTuples (littéraux de tuple)

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

Utilisé comme

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

Comme d'autres l'ont mentionné, la raison pour laquelle Dictionary<TKey,TVal>.AddRangen'est pas implémenté est qu'il existe plusieurs façons de gérer les cas où vous avez des doublons. Ceci est également le cas pour Collectionou interfaces telles que IDictionary<TKey,TVal>, ICollection<T>, etc.

Seulement List<T>met en œuvre, et vous remarquerez que l' IList<T>interface ne pas, pour les mêmes raisons: l' attendu comportement lors de l'ajout d'une plage de valeurs à une collection peut varier largement, selon le contexte.

Le contexte de votre question suggère que vous ne vous inquiétez pas des doublons, auquel cas vous avez une simple alternative oneliner, en utilisant Linq:

MethodThatReturnAnotherDic().ToList.ForEach(kvp => dic.Add(kvp.Key, kvp.Value));
Ama
la source