C # KeyNotFoundException com mais informações

Aug 29 2020

Discurso preliminar:

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

Oh. Minhas.

Então, finalmente cansei de ver como as informações de exceção padrão são inúteis .

Durante uma parte do projeto, não usarei mais a this[TKey key]propriedade para acessar meus Dictionaryarquivos, mas usarei um método de extensão personalizado que, na verdade, tenta me dizer que o WTF deu errado.

Justificativa preliminar:

O sistema que tenho tem um grande pacote de "testes de integração", além de testes de unidade mais facilmente depuráveis, e se as coisas vão para o sul, é difícil entender o que deu errado, se tudo o que você tem é um rastreamento de pilha bastante inespecífico e uma mensagem contando vocês:

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

Eu observaria que este sistema depende muito de mapeamentos (string), onde algum tipo de erro de configuração pode facilmente acionar tais erros em lugares inesperados.

Especialmente, há muitos lugares no código, onde diferentes dicionários serão consultados na mesma parte / linha do código , tornando o rastreamento de pilha basicamente inútil para localizar o problema exato. Pensar:

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

Também observarei que comecei codificando as TryGetValuecoisas manualmente , mas isso ficou ilegível bem rápido, então estou ansioso para tentar uma abordagem um pouco menos específica, mas com sorte mais utilizável.

Requisitos

  • A exceção "Chave não encontrada" deve conter informações relativas a:

    • nome local do dicionário
    • número de itens no dicionário
    • valor da chave
  • As informações devem ser registradas pela estrutura de registro (NLog) sem mais alterações.

  • Sinto que devo observar explicitamente o seguinte: o vazamento potencial de keyinformações de valor é irrelevante para este sistema. Por favor, não comente wrt. este aspecto.

Código

Se aplicável, todos os meus snippets basicamente se enquadram https://opensource.org/licenses/unlicense no caso improvável de alguém querer copiar isso.

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

Questões

Além do feedback geral:

  • Existe algum nome lógico para este método de extensão?
  • Será que você fazer este um método de extensão?
  • Existe alguma outra boa maneira de capturar o nome da variável do dicionário?
  • Ordem dos parâmetros? Como evitar bagunçar a ordem de string nameOfDicte keyse TKey == string?

Respostas

2 CharlesNRice Aug 29 2020 at 09:04

Uma maneira diferente de tirar a pele desse gato é embrulhar o dicionário. Algo na linha deste

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

A maioria dos métodos que você estaria apenas encadeando de volta ao innerDictionary, mas a função do indexador você colocaria seu código

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

Você não precisaria alterar o código que está acessando o dicionário e ainda obter as informações de exceção que deseja. Além disso, se você passar o nome no construtor, não precisará se preocupar em continuar a transmiti-lo a cada vez.

Você também pode adicionar ao (s) construtor (es) para torná-lo semelhante aos construtores normais do Dicionário, então você só precisa alterar onde o código cria o dicionário para ser seu wrapper

Eu também acho que envolver uma classe é bastante padrão na programação, veja Decorator Pattern, plus para manutenção futura e alguém que vem atrás de você não precisa saber sobre um novo método de extensão.