【问题标题】:How to tell which key was not found?如何判断未找到哪个密钥?
【发布时间】:2018-09-28 18:52:08
【问题描述】:

我有一个Configuration 类,其中包含许多设置。一个简单的例子:

class Configuration
{
    public string Name { get; set; }
    public string Url { get; set; }
    public string Password { get; set; }
    //There are about 20 of these
}

我想为调用者提供从字符串字典填充此对象的能力,如下所示:

static Configuration CreateFromDictionary(Dictionary<string, string> dict)
{
    try
    {
        return new Configuration
        {
            Name     = dict["Name"],
            Url      = dict["Url"],
            Password = dict["Password"]
        }
    }
    catch(KeyNotFoundException exception)
    {
        throw new ArgumentException("Unable to construct a Configuration from the information given.");
    }
}

这很有效,除了它是一个全有或全无的转换。如果调用者提供的字典大部分都很好,但其中一个条目拼写错误,则转换失败并出现异常。

我希望能够提供更好的异常消息,告诉调用者 哪个 键未找到。似乎有点重要。但我无法从KeyNotFoundException 检索该信息。

我可以编写代码来一次解析字典一行并单独检查每个键,但这似乎真的很痛苦。有没有什么办法可以通过ContainsKeyTryGetValue 一次一行地从异常信息中判断未找到哪个键?

【问题讨论】:

  • 在开始时收集值。

标签: c# .net exception-handling


【解决方案1】:

不幸的是,Dictionary&lt;,&gt; 中的索引器引发的异常“KeyNotFoundException”未在错误消息中提供“key”值。

以下是可用于获取详细异常的通用扩展方法。同样在您的 catch 块中的代码中,您正在吞噬异常。而是将其作为 InnerException 放入,以便正确记录。

static Configuration CreateFromDictionary(Dictionary<string, string> dict)
{
            try
    {
        return new Configuration
        {
            Name = dict.GetValue("Name"),
            Url = dict.GetValue("Url"),
            Password = dict.GetValue("Password")
        }
    }
    catch (KeyNotFoundException ex)
    {
        throw new ArgumentException("Unable to construct a Configuration from the information given.", ex);
    }
 }

public static class ExtensionsUtil
{
    public static Tvalue GetValue<Tvalue, TKey>(this Dictionary<TKey, Tvalue> dict, TKey key)
    {
        Tvalue val = default(Tvalue);
        if (dict.TryGetValue(key, out val))
        {
            return val;
        }
        else
        {
            throw new KeyNotFoundException($"'{key}' not found in the collection.");
        }
    }
}

【讨论】:

    【解决方案2】:

    您可以将键及其映射存储在字典中,并在映射值之前验证输入:

    public static Configuration CreateFromDictionary(Dictionary<string, string> dict)
    {
        var mappings = new Dictionary<string, Action<Configuration, string>>
        {
            [nameof(Name)] = (x, value) => x.Name = value,
            [nameof(Url)] = (x, value) => x.Url = value,
            [nameof(Password)] = (x, value) => x.Password = value,
        };
    
        var missingKeys = mappings.Keys
            .Except(dict.Keys)
            .ToArray();
        if (missingKeys.Any())
            throw new KeyNotFoundException("The given keys are missing: " + string.Join(", ", missingKeys));
    
        return mappings.Aggregate(new Configuration(), (config, mapping) =>
        {
            mapping.Value(config, dict[mapping.Key]);
            return config;
        });
    }
    

    【讨论】:

      【解决方案3】:

      我过去解决这个问题的方法是包装真实字典:

      public class MyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
      {
          public MyDictionary(IDictionary<TKey, TValue> realDictionary)
          {
               _dictionary = realDictionary;
          }
      
          ...
      
          public TValue this[TKey key]
          {
              get
              {
                  try
                  {
                      return _dictionary[key];
                  }
                  catch (KeyNotFoundException e)
                  {
                      throw new KeyNotFoundException($"Key {key} is not in the dictionary", e);
                  }
              }
              ...
          }
          ...
      }
      

      不幸的是,内置异常根本不提供信息。

      【讨论】:

        【解决方案4】:

        到目前为止,我的解决方案是使用扩展方法:

        public static T ParseFor<T>(this IDictionary<string, string> source, string key)
        {
            string val;
            if (!source.TryGetValue(key, out val)) throw new KeyNotFoundException(string.Format("An entry for '{0}' was not found in the dictionary", key));
            try
            {
                return (T)Convert.ChangeType(val, typeof(T));
            }
            catch
            {
                throw new FormatException(string.Format("The value for '{0}' ('{1}') was not in a correct format to be converted to a {2}", key, val, typeof(T).Name));
            }
        }
        

        并像这样使用它:

        return new Configuration
            {
                Name     = dict.ParseFor<string>("Name"),
                Url      = dict.ParseFor<string>("Url"),
                Password = dict.PasreFor<string>("Password")
            }
        

        扩展方法将抛出一个包含键名的有用异常。还具有支持类型转换的好处,例如如果其中一个配置项是bool,它会尝试转换它。

        【讨论】:

        • 如果两个键拼错了怎么办?
        • 你的通用方法只适用于Dictionary&lt;string,string&gt;。此外,您正在吞下 Extension 方法引发的异常。请参阅下面的答案。
        【解决方案5】:

        使用字典的“TryGetValue”方法,抛弃 catch 处理程序。 TryGetValue 的结果如果没有找到键,则为假,如果找到,则为真。如果返回 true,则有一个“out”参数,该值将在哪里。代替您的 catch 处理程序,您只需跟踪哪些键不存在(再次由 TryGetValue 的 false 返回值指示)。

        快速示例:

        string name;
        Configuration config = new Configuration();
        if (dict.TryGetValue("Name", out name))
        {
            config.Name = name;
        }
        else
        {
            throw new ArgumentException("'Name' was not present.");
        }
        

        编辑: 鉴于对问题的澄清,我将修改我的答案:不,不可能从异常中获取该信息。

        【讨论】:

        • 我认为你错过了问题的精神。当然,我可以一一检查密钥,我知道该怎么做。问题是是否有必要。
        • @JohnWu 我想你是对的,我一定错过了问题的精神,我想我仍然是。
        【解决方案6】:

        您可以编写一个构建器来查找存在于您的Configuration 类型中的“字符串”类型的属性,然后遍历这些属性以尝试从给定字典中获取每个属性。这段代码甚至可以是通用的:

        public T Build<T>(Dictionary<string, string> source)
            where T : new()
        {
            var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty;
            var properties = typeof(T).GetProperties(flags)
                 .Where(p => p.PropertyType == typeof(string));
        
            var missingKeys = properties.Select(p => p.Name).Except(source.Keys);
            if (missingKeys.Any())
                throw new FooException($"These keys not found: {String.Join(", ", missingKeys)}");
        
            var obj = new T();
        
            foreach (var p in properties)
                p.SetValue(obj, source[p.Name]);
        
            return obj;
        }
        

        用法:

        // throws FooException if any key  is missing
        var config = Something.Build<Configuration>(dict); 
        

        但通常,配置不包含字符串。

        • 如果Url 不是有效的URI 怎么办?
        • 如果Name 为空怎么办?
        • 如果Password 在字典中是null 怎么办?
        • 如果某些数字设置不是可解析的字符串怎么办?等等。

        在所有这些情况下,您的代码都会很高兴,并且您会在运行时遇到异常。如果您为设置使用特定类型stringUriintDateTime 等,情况会变得更好。并尽快验证这些值。此外,通常某些设置是可选的 - 如果字典或您正在使用的任何来源中缺少此设置,则不应抛出此设置。

        public class Configuration
        {
            public string Name { get; set; }
            public Uri Url { get; set; }
            public int RequiredNumber { get; set; }
            public int? NotRequiredNumber { get; set; }
        }
        

        然后你可以创建一组扩展方法来尝试从字典中获取和解析值:

        public static Uri GetUri(this Dictionary<string, string> source, string key)
        {
            if (!source.TryGetValue(key, out string value))
                throw new ConfigurationException($"{key} not found");
        
            if (!Uri.TryCreate(value, UriKind.Absolute, out Uri result))
                throw new ConfigurationException($"{key} is not a valid uri: {value}");
        
            return result;
        }
        

        创建配置如下所示

        var config = new Configuration {
            Name = dict.GetString("Name"),
            Url = dict.GetUri("Uri"),
            RequiredNumber = dict.GetInt("RequiredNumber"),
            NotRequiredNumber = dict.GetNullableInt("NotRequiredNumber")
        };
        

        如果配置错误,您的代码将很快失败。如果创建了配置,您将非常安全。

        【讨论】:

          【解决方案7】:

          你可以做什么。

            static Configuration CreateConfiguration(Dictionary<string,string> dict)
              {
                  try
                  {
                      return
                          new Configuration
                          {
                              FirstName = dict.getValue("FirstName"),
                              LastName = dict.getValue("LastName"),
                              ID = dict.getValue("ID")                        
                          };
                  }
                  catch (Exception ex)
                  {
                      throw ex;
                  }
              }
          
              public static class Extension
          {
              public static string getValue(this Dictionary<string, String> dict, string keyName)
              {
                  string data = null;
                  var result = dict.TryGetValue(keyName, out data);
                  if (!result)
                      throw new KeyNotFoundException($"The given key '{keyName}' was not present in the dictionary. keyname is not found");
                  return data;
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2010-12-30
            • 1970-01-01
            • 1970-01-01
            • 2018-12-01
            • 2019-10-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多