C # KeyNotFoundException avec plus d'informations

Aug 29 2020

Coup de gueule préliminaire:

System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.

Oh. Ma.

Donc j'ai finalement eu marre de la façon dont les informations d'exception par défaut sont au-delà de l'inutilité .

Pour une partie du projet, je n'utiliserai généralement plus la this[TKey key]propriété pour accéder à mes Dictionarys mais j'utiliserai une méthode d'extension personnalisée qui essaie en fait de me dire que WTF a mal tourné.

Justification préliminaire:

Le système que j'ai a une grande suite de "tests d'intégration" en plus de tests unitaires plus faciles à déboguer, et si les choses vont vers le sud, il est difficile de comprendre ce qui n'a pas fonctionné, si tout ce que vous avez est une trace de pile plutôt non spécifique et un message indiquant toi:

System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.

Je noterais que ce système repose fortement sur les mappages (de chaînes), où une sorte d'erreur de configuration peut facilement déclencher de telles erreurs dans des endroits inattendus.

Surtout, il y a beaucoup d'endroits dans le code, où différents dictionnaires seront consultés dans la même partie / ligne de code , ce qui rend le stacktrace pratiquement inutile pour identifier le problème exact. Pense:

var attributeValue = model[Config.PartX].attributes[Config.Attribute42];

Je noterai également que j'ai commencé avec le codage manuel du TryGetValuetruc, mais cela est devenu illisible assez rapidement, donc j'ai hâte d'essayer une approche un peu moins spécifique, mais plus, espérons-le, utilisable.

Exigences

  • L'exception "Clé introuvable" doit contenir des informations relatives à:

    • nom local du dictionnaire
    • nombre d'éléments dans le dictionnaire
    • valeur de la clé
  • Les informations doivent être enregistrées par le cadre de journalisation (NLog) sans autre manipulation.

  • Je pense que je dois explicitement noter ceci: la fuite potentielle d' keyinformations de valeur est au-delà de toute pertinence pour ce système. Veuillez ne pas commenter. cet aspect.

Code

Le cas échéant, tous mes extraits relèvent essentiellement de https://opensource.org/licenses/unlicense dans le cas peu probable, quelqu'un voudrait copier ceci.

namespace MY.Project
{
    public static class DictionaryHelper
    {
        /// <summary>
        /// Same as `TValue this[TKey key] { get; }`, but with a better exception message
        /// containing the dictionary "name" (needs to be provided), number of entries and key value
        /// </summary>
        /// <returns>Value if found, throws KeyNotFoundException otherwise</returns>
        public static TValue GetExistingValue<TKey, TValue>(this IDictionary<TKey, TValue> dict, string nameOfDict, TKey key)
        {
            if (!dict.TryGetValue(key, out var val))
            {
                throw CreateKeyNotFoundException(dict, nameOfDict, dict.Count, key);
            }
            return val;
        }

        /// <see cref="GetExistingValue"/> for `IDictionary` above.
        public static TValue GetExistingValue<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dict, string nameOfDict, TKey key)
        {
            if (!dict.TryGetValue(key, out var val))
            {
                throw CreateKeyNotFoundException(dict, nameOfDict, dict.Count, key);
            }
            return val;
        }

        /// <summary>
        /// Provide separate explicit overload for the `Dictionary` class because this class implements both
        /// the IDict and IReadOnlyDict interface, making the overload ambiguous otherwise
        /// </summary>
        public static TValue GetExistingValue<TKey, TValue>(this Dictionary<TKey, TValue> dict, string nameOfDict, TKey key)
        {
            return GetExistingValue((IDictionary<TKey, TValue>)dict, nameOfDict, key);
        }

        private static KeyNotFoundException CreateKeyNotFoundException<T, TCount, TKey>(T dict, string nameOfDict, TCount count, TKey key)
        {
            return new KeyNotFoundException(
                    $"{nameOfDict} ({dict.GetType()}) (with #{count} entries) does not contain key <{key}>!"
                );
        }
    }

Des questions

Au-delà des commentaires généraux:

  • Y a-t-il un nom sain pour cette méthode d'extension?
  • Souhaitez- vous en faire une méthode d'extension?
  • Existe-t-il un autre bon moyen de capturer le nom de la variable du dictionnaire?
  • Ordre des paramètres? Comment éviter de gâcher l'ordre de string nameOfDictet keysi TKey == string?

Réponses

2 CharlesNRice Aug 29 2020 at 09:04

Une autre façon d'écorcher ce chat est d'envelopper le dictionnaire. Quelque chose du genre

public class DictionarWrapper<TKey, TValue> : IDictionary<TKey, TValue>
{
    private readonly IDictionary<TKey, TValue> innerDictionary;
    private readonly string name;
    
    public DictionarWrapper(IDictionary<TKey, TValue> innerDictionary, string name)
    {
        // could throw if null or set to empty
        this.name = name ?? string.Empty;
        this.innerDictionary = innerDictionary;
    }

    public DictionarWrapper(string name) : this(new Dictionary<TKey, TValue>(), name)
    {
    }

La plupart des méthodes que vous enchaîneriez simplement dans le innerDictionary mais la fonction d'indexation que vous mettriez votre code

public TValue this[TKey key] 
{
     get {
         // either trap error or write own TryGetValue and skip calling the normal inner indexer 
         try
         {
            return innerDictionary[key];
         }
         catch (KeyNotFoundException ex)
         {                     
             throw new KeyNotFoundException($"{name} ({innerDictionary.GetType()}) (with #{Count} entries) does not contain key <{key}>!", ex);
         }
     }
     set => innerDictionary[key] = value;
}

Vous n'avez pas besoin de modifier votre code qui accède au dictionnaire et d'obtenir toujours les informations d'exception souhaitées. De plus, si vous passez le nom dans le constructeur, vous n'avez pas à vous soucier de continuer à le transmettre à chaque fois.

Vous pouvez également ajouter au (x) constructeur (s) pour le faire comme les constructeurs de dictionnaire normaux, il vous suffit alors de changer l'endroit où le code crée le dictionnaire pour qu'il soit votre wrapperr

J'ai aussi le sentiment que la création d'une classe est assez standard dans la programmation, voir Decorator Pattern, plus pour la maintenance future et quelqu'un qui vient après vous n'a pas besoin de connaître une nouvelle méthode d'extension.