web-dev-qa-db-fra.com

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

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.

De plus, je me retrouve souvent à avoir 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 façon meilleure et plus élégante d'utiliser les dictionnaires, mais je suis à court.

19
Adam B

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 renvoyant null ou nil 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 variante Try. 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 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.


Faut-il décourager les variables out?

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 concurrent, vous ne pouvez pas utiliser de variables out aux limites de la 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 construire 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.

23
Berin Loritsch

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.

23
David Arno

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 idiomatique C #. Cependant, il y a 3 options fournies en C # pour travailler avec un dictionnaire, donc si vous êtes sûr que vous utilisez 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, donc je vais 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 pour Dictionary 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. 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 donne 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 comment 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.

12
BrianH

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 utiliser les idiomes fournis par un langage le cas échéant, comme le modèle Try, mais cela ne signifie pas que vous devez niquement utiliser ce que le langage fournit. 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.

10
Karl Bielefeldt

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 TryGetValue qui retourne 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:

https://stackoverflow.com/questions/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 les dictionnaires, mais je suis à court.

Demandez-vous "supposons que j'avais une classe autre que Dictionary 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!

5
Eric Lippert

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

Une approche "moins sale" pourrait être d'utiliser à la place ContainsKey() comme vérification.

4
Peregrine

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 au nom confus qui sacrifie la vitesse d'insertion à la vitesse de recherche, mais qui brille si vous regardez avec un ensemble fixe et pré-trié). J'ai eu un cas où le remplacement d'un Dictionary par un SortedDictionary a entraîné une exécution plus rapide d'un ordre de grandeur (mais cela pourrait aussi se produire dans l'autre sens).

2
Vilx-

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

1
Martin Maat