Existe-t-il une meilleure façon d'utiliser les dictionnaires C # que TryGetValue?

19

Je me retrouve souvent à chercher des questions en ligne, et de nombreuses solutions incluent des dictionnaires. Cependant, chaque fois que j'essaie de les implémenter, j'obtiens cette horrible odeur dans mon code. Par exemple, chaque fois que je veux utiliser une valeur:

int x;
if (dict.TryGetValue("key", out x)) {
    DoSomethingWith(x);
}

C'est 4 lignes de code pour faire essentiellement ce qui suit: DoSomethingWith(dict["key"])

J'ai entendu dire que l'utilisation du mot-clé out est un anti-modèle car il fait que les fonctions modifient leurs paramètres.

En outre, je me retrouve souvent besoin d'un dictionnaire "inversé", où je retourne les clés et les valeurs.

De même, je voudrais souvent parcourir les éléments d'un dictionnaire et me retrouver à convertir des clés ou des valeurs en listes, etc. pour faire mieux.

J'ai l'impression qu'il y a presque toujours une meilleure façon, plus élégante, d'utiliser des dictionnaires, mais je suis à court.

Adam B
la source
7
D'autres façons peuvent exister mais j'utilise généralement une ContainsKey avant d'essayer d'obtenir une valeur.
Robbie Dee
2
Honnêtement, à quelques exceptions près, si vous savez ce que vos clés de dictionnaire sont à l' avance, il y a sans doute est une meilleure façon. Si vous ne le savez pas, les touches dict ne doivent généralement pas être codées en dur du tout. Les dictionnaires sont utiles pour travailler avec des objets ou des données semi-structurés où les champs sont au moins principalement orthogonaux à l'application. OMI, plus ces domaines se rapprochent de concepts commerciaux / de domaine pertinents, moins les dictionnaires deviennent utiles pour travailler avec ces concepts.
svidgen
6
@RobbieDee: vous devez cependant être prudent lorsque vous faites cela, car cela crée une condition de concurrence. Il est possible que la clé soit supprimée entre l'appel de ContainsKey et l'obtention de la valeur.
whatsisname
12
@whatsisname: Dans ces conditions, un ConcurrentDictionary serait plus approprié. Les collections dans l' System.Collections.Genericespace de noms ne sont pas thread-safe .
Robert Harvey
4
Un bon 50% du temps, je vois quelqu'un utiliser un Dictionary, ce qu'il voulait vraiment, c'était un nouveau cours. Pour ces 50% (seulement), c'est une odeur de design.
BlueRaja - Danny Pflughoeft

Réponses:

23

Les dictionnaires (C # ou autre) sont simplement un conteneur où vous recherchez une valeur basée sur une clé. Dans de nombreuses langues, il est plus correctement identifié comme une carte, l'implémentation la plus courante étant un HashMap.

Le problème à considérer est ce qui se passe lorsqu'une clé n'existe pas. Certaines langues se comportent en retournant nullounil ou une autre valeur équivalente. La valeur par défaut silencieuse à une valeur au lieu de vous informer qu'une valeur n'existe pas.

Pour le meilleur ou pour le pire, les concepteurs de bibliothèques C # ont trouvé un idiome pour gérer le comportement. Ils ont estimé que le comportement par défaut pour rechercher une valeur qui n'existe pas est de lever une exception. Si vous souhaitez éviter les exceptions, vous pouvez utiliser la Tryvariante. C'est la même approche qu'ils utilisent pour analyser les chaînes en entiers ou en objets date / heure. Essentiellement, l'impact est le suivant:

T count = int.Parse("12T45"); // throws exception

if (int.TryParse("12T45", out count))
{
    // Does not throw exception
}

Et cela a été reporté au dictionnaire, dont l'indexeur délègue Get(index):

var myvalue = dict["12345"]; // throws exception
myvalue = dict.Get("12345"); // throws exception

if (dict.TryGet("12345", out myvalue))
{
    // Does not throw exception
}

C'est simplement la façon dont la langue est conçue.


Devrait out il décourager les variables?

C # n'est pas la première langue à les avoir, et ils ont leur raison d'être dans des situations spécifiques. Si vous essayez de créer un système hautement simultané, vous ne pouvez pas utiliser de outvariables aux limites de concurrence.

À bien des égards, s'il existe un idiome qui est adopté par les fournisseurs de langue et de bibliothèque de base, j'essaie d'adopter ces idiomes dans mes API. Cela rend l'API plus cohérente et à l'aise dans cette langue. Une méthode écrite en Ruby ne ressemblera donc pas à une méthode écrite en C #, C ou Python. Ils ont chacun une façon préférée de créer du code, et travailler avec cela aide les utilisateurs de votre API à l'apprendre plus rapidement.


Les cartes en général sont-elles un anti-modèle?

Ils ont leur but, mais souvent, ils peuvent être la mauvaise solution pour le but que vous avez. Surtout si vous avez besoin d'une cartographie bidirectionnelle. Il existe de nombreux conteneurs et façons d'organiser les données. Il existe de nombreuses approches que vous pouvez utiliser, et parfois vous devez réfléchir un peu avant de choisir ce conteneur.

Si vous avez une très courte liste de valeurs de mappage bidirectionnel, vous n'aurez peut-être besoin que d'une liste de tuples. Ou une liste de structures, où vous pouvez tout aussi facilement trouver la première correspondance de chaque côté du mappage.

Pensez au domaine problématique et choisissez l'outil le plus approprié pour le travail. S'il n'y en a pas, créez-le.

Berin Loritsch
la source
4
"alors vous pourriez avoir besoin seulement d'une liste de tuples. Ou d'une liste de structures, où vous pouvez tout aussi facilement trouver la première correspondance de chaque côté du mappage." - S'il y a des optimisations pour les petits ensembles, elles doivent être faites dans la bibliothèque, et non tamponnées en caoutchouc dans le code utilisateur.
Blrfl
Si vous choisissez une liste de tuples ou un dictionnaire, il s'agit d'un détail d'implémentation. Il s'agit de comprendre votre domaine problématique et d'utiliser le bon outil pour le travail. Évidemment, une liste existe et un dictionnaire existe. Dans le cas courant, le dictionnaire est correct, mais peut-être pour une ou deux applications, vous devez utiliser la liste.
Berin Loritsch
3
C'est vrai, mais si le comportement est toujours le même, cette détermination devrait être dans la bibliothèque. J'ai parcouru des implémentations de conteneurs dans d'autres langages qui changeront d'algorithme si on leur dit a priori que le nombre d'entrées sera petit. Je suis d'accord avec votre réponse, mais une fois que cela a été compris, cela devrait être dans une bibliothèque, peut-être en tant que SmallBidirectionalMap.
Blrfl
5
"Pour le meilleur ou pour le pire, les concepteurs de bibliothèques C # ont trouvé un idiome pour gérer le comportement." - Je pense que vous avez frappé le clou sur la tête: ils ont "trouvé" un idiome, au lieu d'utiliser un existant largement utilisé, qui est un type de retour facultatif.
Jörg W Mittag
3
@ JörgWMittag Si vous parlez de quelque chose comme ça Option<T>, alors je pense que cela rendrait la méthode beaucoup plus difficile à utiliser en C # 2.0, car elle n'avait pas de correspondance de modèle.
svick
21

Quelques bonnes réponses ici sur les principes généraux des tables de hachage / dictionnaires. Mais je pensais que je toucherais à votre exemple de code,

int x;
if (dict.TryGetValue("key", out x)) 
{
    DoSomethingWith(x);
}

À partir de C # 7 (qui, je pense, a environ deux ans), cela peut être simplifié pour:

if (dict.TryGetValue("key", out var x))
{
    DoSomethingWith(x);
}

Et bien sûr, il pourrait être réduit à une seule ligne:

if (dict.TryGetValue("key", out var x)) DoSomethingWith(x);

Si vous avez une valeur par défaut lorsque la clé n'existe pas, elle peut devenir:

DoSomethingWith(dict.TryGetValue("key", out var x) ? x : defaultValue);

Vous pouvez donc obtenir des formulaires compacts en utilisant des ajouts de langage raisonnablement récents.

David Arno
la source
1
Bon appel à la syntaxe v7, jolie petite option pour devoir couper la ligne de définition supplémentaire tout en permettant l'utilisation de var +1
BrianH
2
Notez également que s'il "key"n'est pas présent, xsera initialisé àdefault(TValue)
Peter Duniho
Cela pourrait aussi être une extension générique, invoquée comme"key".DoSomethingWithThis()
Ross Presser
Je préfère grandement les conceptions qui utilisent l' getOrElse("key", defaultValue) objet Null est toujours mon motif préféré. Travaillez de cette façon et vous ne vous souciez pas si les TryGetValueretours sont vrais ou faux.
candied_orange
1
J'ai l'impression que cette réponse mérite un avertissement que l'écriture de code compact pour le plaisir d'être compact est mauvaise. Cela peut rendre votre code beaucoup plus difficile à lire. De plus, si je me souviens bien, TryGetValuen'est pas atomique / thread-safe donc vous pouvez tout aussi facilement effectuer une vérification de l'existence et une autre pour saisir et opérer la valeur
Marie
12

Ce n'est ni une odeur de code ni un anti-modèle, car l'utilisation de fonctions de style TryGet avec un paramètre out est un C # idiomatique. Cependant, il y a 3 options fournies en C # pour travailler avec un dictionnaire, donc si vous êtes sûr d'utiliser le bon pour votre situation. Je pense que je sais d'où vient la rumeur d'un problème d'utilisation d'un paramètre de sortie, je vais donc gérer cela à la fin.

Quelles fonctionnalités utiliser lorsque vous travaillez avec un dictionnaire C #:

  1. Si vous êtes sûr que la clé sera dans le dictionnaire, utilisez la propriété Item [TKey]
  2. Si la clé doit généralement être dans le dictionnaire, mais qu'il est mauvais / rare / problématique qu'elle ne soit pas là, vous devez utiliser Try ... Catch pour qu'une erreur soit générée et que vous puissiez ensuite essayer de gérer l'erreur avec élégance
  3. Si vous n'êtes pas sûr que la clé sera dans le dictionnaire, utilisez TryGet avec le paramètre out

Pour justifier cela, il suffit de se référer à la documentation du dictionnaire TryGetValue, sous "Remarques" :

Cette méthode combine les fonctionnalités de la méthode ContainsKey et la propriété Item [TKey].

...

Utilisez la méthode TryGetValue si votre code tente fréquemment d'accéder à des clés qui ne figurent pas dans le dictionnaire. L'utilisation de cette méthode est plus efficace que la capture de la KeyNotFoundException levée par la propriété Item [TKey].

Cette méthode approche une opération O (1).

La raison principale pour laquelle TryGetValue existe est d'agir comme un moyen plus pratique d'utiliser ContainsKey et Item [TKey], tout en évitant d'avoir à rechercher deux fois dans le dictionnaire - donc prétendre qu'il n'existe pas et faire les deux choses qu'il fait manuellement est plutôt délicat. choix.

En pratique, j'ai rarement utilisé un dictionnaire brut, en raison de cette maxime simple: choisissez la classe / le conteneur le plus générique qui vous offre les fonctionnalités dont vous avez besoin . Le dictionnaire n'a pas été conçu pour rechercher par valeur plutôt que par clé (par exemple), donc si c'est quelque chose que vous voulez, il peut être plus judicieux d'utiliser une autre structure. Je pense que j'ai peut-être utilisé Dictionary une fois dans le dernier projet de développement d'un an que j'ai fait, simplement parce que c'était rarement le bon outil pour le travail que j'essayais de faire. Le dictionnaire n'est certainement pas le couteau suisse de la boîte à outils C #.

Quel est le problème avec nos paramètres?

CA1021: Évitez les paramètres

Bien que les valeurs de retour soient courantes et largement utilisées, l'application correcte des paramètres out et ref nécessite des compétences de conception et de codage intermédiaires. Les architectes de bibliothèque qui conçoivent pour un public général ne doivent pas s'attendre à ce que les utilisateurs maîtrisent l'utilisation des paramètres out ou ref.

Je suppose que c'est là que vous avez entendu que les paramètres de sortie étaient quelque chose comme un anti-modèle. Comme pour toutes les règles, vous devriez lire de plus près pour comprendre le `` pourquoi '', et dans ce cas, il y a même une mention explicite de la façon dont le modèle Try ne viole pas la règle :

Les méthodes qui implémentent le modèle Try, telles que System.Int32.TryParse, ne déclenchent pas cette violation.

BrianH
la source
Toute cette liste de directives est quelque chose que j'aimerais que plus de gens lisent. Pour ceux qui suivent, en voici la racine. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone
10

Il manque au moins deux méthodes dans les dictionnaires C # qui, à mon avis, nettoient considérablement le code dans de nombreuses situations dans d'autres langues. La première renvoie un Option, qui vous permet d'écrire du code comme celui-ci dans Scala:

dict.get("key").map(doSomethingWith)

La seconde renvoie une valeur par défaut spécifiée par l'utilisateur si la clé n'est pas trouvée:

doSomethingWith(dict.getOrElse("key", "key not found"))

Il y a quelque chose à dire pour l' utilisation des idiomes une langue fournit le cas échéant, comme le Trymodèle, mais cela ne signifie pas que vous devez seulement utiliser ce que la langue offre. Nous sommes programmeurs. Il est normal de créer de nouvelles abstractions pour rendre notre situation spécifique plus facile à comprendre, surtout si cela élimine beaucoup de répétitions. Si vous avez fréquemment besoin de quelque chose, comme des recherches inversées ou une itération à travers des valeurs, faites-le. Créez l'interface que vous souhaitez avoir.

Karl Bielefeldt
la source
J'aime la 2e option. Peut-être que je devrai écrire une méthode d'extension ou deux :)
Adam B
2
du côté positif, les méthodes d'extension c # signifient que vous pouvez les implémenter vous
jk.
5

C'est 4 lignes de code pour faire essentiellement ce qui suit: DoSomethingWith(dict["key"])

Je reconnais que cela n'est pas élégant. Un mécanisme que j'aime utiliser dans ce cas, où la valeur est un type struct, est:

public static V? TryGetValue<K, V>(
      this Dictionary<K, V> dict, K key) where V : struct => 
  dict.TryGetValue(key, out V v)) ? new V?(v) : new V?();

Nous avons maintenant une nouvelle version de TryGetValuece retour int?. Nous pouvons alors faire une astuce similaire pour étendre T?:

public static void DoIt<T>(
      this T? item, Action<T> action) where T : struct
{
  if (item != null) action(item.GetValueOrDefault());
}

Et maintenant, assemblez-le:

dict.TryGetValue("key").DoIt(DoSomethingWith);

et nous en sommes à une seule déclaration claire.

J'ai entendu dire que l'utilisation du mot-clé out est un anti-modèle car il fait que les fonctions modifient leurs paramètres.

Je serais un peu moins fortement formulé et je dirais qu'éviter les mutations lorsque c'est possible est une bonne idée.

Je me retrouve souvent besoin d'un dictionnaire "inversé", où je retourne les clés et les valeurs.

Ensuite, implémentez ou obtenez un dictionnaire bidirectionnel. Ils sont simples à écrire, ou il existe de nombreuses implémentations disponibles sur Internet. Il y a un tas d'implémentations ici, par exemple:

/programming/268321/bidirectional-1-to-1-dictionary-in-c-sharp

De même, je voudrais souvent parcourir les éléments d'un dictionnaire et me retrouver à convertir des clés ou des valeurs en listes, etc. pour faire mieux.

Bien sûr, nous le faisons tous.

J'ai l'impression qu'il y a presque toujours une meilleure façon, plus élégante, d'utiliser des dictionnaires, mais je suis à court.

Demandez-vous "supposons que j'ai eu une classe autre que Dictionarycelle qui a implémenté l'opération exacte que je souhaite faire; à quoi ressemblerait cette classe?" Ensuite, une fois que vous avez répondu à cette question, implémentez cette classe . Vous êtes programmeur informatique. Résolvez vos problèmes en écrivant des programmes informatiques!

Eric Lippert
la source
Merci. Pensez-vous pouvoir expliquer un peu plus ce que fait réellement la méthode d'action? Je suis nouveau dans les actions.
Adam B
1
@AdamB une action est un objet qui représente la possibilité d'appeler une méthode void. Comme vous le voyez, du côté de l'appelant, nous transmettons le nom d'une méthode void dont la liste de paramètres correspond aux arguments de type générique de l'action. Du côté de l'appelant, l'action est invoquée comme toute autre méthode.
Eric Lippert
1
@AdamB: Vous pouvez penser Action<T>à être très similaire à interface IAction<T> { void Invoke(T t); }, mais avec des règles très clémentes sur ce qui "implémente" cette interface et sur la façon dont elle Invokepeut être invoquée. Si vous voulez en savoir plus, découvrez les «délégués» en C #, puis découvrez les expressions lambda.
Eric Lippert
Ok je pense que je l'ai. Donc, l'action vous permet de mettre une méthode en tant que paramètre pour DoIt (). Et appelle cette méthode.
Adam B
@AdamB: Exactement raison. Ceci est un exemple de "programmation fonctionnelle avec des fonctions d'ordre supérieur". La plupart des fonctions prennent les données comme arguments; les fonctions d'ordre supérieur prennent des fonctions comme arguments, puis font des choses avec ces fonctions. Au fur et à mesure que vous en apprendrez plus sur C #, vous verrez que LINQ est entièrement implémenté avec des fonctions d'ordre supérieur.
Eric Lippert
4

La TryGetValue()construction n'est nécessaire que si vous ne savez pas si "clé" est présente ou non dans le dictionnaire, sinon elle DoSomethingWith(dict["key"])est parfaitement valide.

Une approche «moins sale» pourrait être plutôt utilisée ContainsKey()comme chèque.

Peregrine
la source
6
"Une approche" moins sale "pourrait être d'utiliser à la place ContainsKey () comme vérification." Je ne suis pas d'accord. Aussi sous-optimal que tel TryGetValue, au moins, il est difficile d'oublier de gérer le cas vide. Idéalement, je souhaite que cela renvoie simplement une option.
Alexander - Reinstate Monica
1
Il y a un problème potentiel avec l' ContainsKeyapproche pour les applications multithread, qui est la vulnérabilité de temps de vérification à l'heure d'utilisation (TOCTOU). Que faire si un autre thread supprime la clé entre les appels à ContainsKeyet GetValue?
David Hammen
2
@JAD Optionals compose. Vous pouvez avoir unOptional<Optional<T>>
Alexander - Rétablir Monica
2
@DavidHammen Pour être clair, vous auriez besoin TryGetValuede ConcurrentDictionary. Le dictionnaire régulier ne serait pas synchronisé
Alexander - Reinstate Monica
2
@JAD Non, (le type facultatif de Java souffle, ne me lancez pas). Je parle de façon plus abstraite
Alexander - Reinstate Monica
2

D'autres réponses contiennent d'excellents points, donc je ne les reformulerai pas ici, mais je me concentrerai plutôt sur cette partie, qui semble avoir été largement ignorée jusqu'à présent:

De même, je voudrais souvent parcourir les éléments d'un dictionnaire et me retrouver à convertir des clés ou des valeurs en listes, etc. pour faire mieux.

En fait, il est assez facile d'itérer sur un dictionnaire, car il implémente IEnumerable:

var dict = new Dictionary<int, string>();

foreach ( var item in dict ) {
    Console.WriteLine("{0} => {1}", item.Key, item.Value);
}

Si vous préférez Linq, cela fonctionne aussi très bien:

dict.Select(x=>x.Value).WhateverElseYouWant();

Dans l'ensemble, je ne trouve pas que les dictionnaires soient un contre-modèle - ils sont juste un outil spécifique avec des utilisations spécifiques.

En outre, pour encore plus de détails, consultez SortedDictionary(utilise les arbres RB pour des performances plus prévisibles) et SortedList(qui est également un dictionnaire nommé de manière confuse qui sacrifie la vitesse d'insertion pour la vitesse de recherche, mais brille si vous regardez avec un fixe, pré- ensemble trié). J'ai eu un cas où le remplacement d'un Dictionaryavec un a SortedDictionaryentraîné une exécution plus rapide d'un ordre de grandeur (mais cela pourrait aussi se produire dans l'autre sens).

Vilx-
la source
1

Si vous pensez que l'utilisation d'un dictionnaire est gênant, ce n'est peut-être pas le bon choix pour votre problème. Les dictionnaires sont excellents, mais comme l'a remarqué un commentateur, ils sont souvent utilisés comme raccourci pour quelque chose qui aurait dû être une classe. Ou le dictionnaire lui-même peut être une méthode de stockage de base, mais il devrait y avoir une classe wrapper autour de lui pour fournir les méthodes de service souhaitées.

On a beaucoup parlé de Dict [clé] contre TryGet. Ce que j'utilise beaucoup, c'est d'itérer sur le dictionnaire en utilisant KeyValuePair. Apparemment, c'est une construction moins connue.

L'avantage principal d'un dictionnaire est qu'il est vraiment rapide par rapport aux autres collections à mesure que le nombre d'éléments augmente. Si vous devez vérifier si une clé existe beaucoup, vous pouvez vous demander si votre utilisation d'un dictionnaire est appropriée. En tant que client, vous devez généralement savoir ce que vous y mettez et donc ce qu'il serait sûr d'interroger.

Martin Maat
la source