【问题标题】:.NET Dictionary: get or create new.NET 字典:获取或创建新的
【发布时间】:2013-04-18 01:23:48
【问题描述】:

我经常发现自己创建了一个带有非平凡值类的Dictionary(例如List),然后在填充数据时总是编写相同的代码模式。

例如:

var dict = new Dictionary<string, List<string>>();
string key = "foo";
string aValueForKey = "bar";

也就是说,我想将"bar" 插入到与键"foo" 对应的列表中,其中键"foo" 可能不会映射到任何东西。

这是我使用不断重复模式的地方:

List<string> keyValues;
if (!dict.TryGetValue(key, out keyValues))
  dict.Add(key, keyValues = new List<string>());
keyValues.Add(aValueForKey);

有没有更优雅的方式来做到这一点?

对此问题没有答案的相关问题:

【问题讨论】:

  • 如果key存在但List为空怎么办?
  • @Barabba 一般来说,我认为添加空值会被认为是不合适的,您会期望代码只是轰炸并修复添加空键的错误,而不是试图在这里处理它。
  • @Servy 好的,但是如果我们从第三方来源获得结果列表怎么办?唯一要处理的是处理空值,我错了吗?它每天都发生在我身上:)
  • @Barabba 那么你应该在将它添加到字典之前检查其他地方返回的列表是否为空,以确保你没有添加空值。

标签: c# .net dictionary insert-update


【解决方案1】:

我们对此的看法略有不同,但效果相似:

public static TValue GetOrCreate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) 
    where TValue : new()
{
    if (!dict.TryGetValue(key, out TValue val))
    {
        val = new TValue();
        dict.Add(key, val);
    }

    return val;
}

调用:

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

List<int> numbers = dictionary.GetOrCreate("key");

它利用公共无参数构造函数的通用约束:where TValue : new()

为了帮助发现,除非扩展方法非常特定于一个狭窄的问题,否则我们倾向于将扩展方法放在它们所扩展类型的命名空间中,在这种情况下:

namespace System.Collections.Generic

大多数时候,使用该类型的人在顶部定义了 using 语句,因此 IntelliSense 还会找到在您的代码中定义的扩展方法。

【讨论】:

  • @Darthenius 不,第一种方法是特定于列表的(我们还有其他用于 HashSet 和其他 Dictionary&lt;&gt; 项目的方法)。我不记得确切的推理,但我认为因为 List&lt;&gt; 还包含一个泛型类型,这样做更适合类型推断。
  • @Darthenius 好吧,我刚刚回到我们的代码并注释掉了我们拥有的List&lt;&gt; 版本,它不再有用,它在功能上与上面的标准版本相同。所以我把它删除了。它一定是我第一次编码时不小心留下的。
  • @Darthenius 好吧,它们在功能上并不完全等效,所以我没有删除它们。但是,就这个问题而言,它并不相关。它们的不同之处在于它们检查的是 null 项,而不是字典中没有的项,因此带有 null 项的键也会创建一个。
  • @BKSpureon 这是一个generic constraint,要求TValue类型有一个公共的无参数构造函数。
  • @void.pointer 哦,我明白了,在这种情况下,它可能不会“创建”,所以可能是不同的方法名称,例如 GetOrDefault
【解决方案2】:

和这么多编程问题一样,当你发现自己做了很多事情时,把它重构为一个方法:

public static void MyAdd<TKey, TCollection, TValue>(
    this Dictionary<TKey, TCollection> dictionary, TKey key, TValue value)
    where TCollection : ICollection<TValue>, new()
{
    TCollection collection;
    if (!dictionary.TryGetValue(key, out collection))
    {
        collection = new TCollection();
        dictionary.Add(key, collection);
    }
    collection.Add(value);
}

【讨论】:

  • @Darthenius 这只是一个不同的看法。它将方法的范围缩小到将项目添加到任何实现ICollection 的集合中。要点是将代码存放在单独的位置以供重复使用。我不认为他试图改进已经提供的代码。
  • @Darthenius 如果你愿意,你也可以返回集合,假设这是一件特别常见的事情。根据我使用Dictionary&lt;TKey, SomeCollection&gt; 模式的经验,我不会经常一次为同一个键添加多个值。如果这是经常发生的事情,您可以添加另一个重载,在其中您接受 IEnumerable&lt;TValue&gt; 并将它们全部添加到方法中。
【解决方案3】:

对于更多读者,这里有一些我认为合适的扩展。如果您需要检查是否添加了一个值,您也可以使用 out 参数做一些事情,但我认为您可以使用 containskey 或其他东西。

您可以使用GetOrAddNew 检索项目,或创建并将其添加到字典中。您可以使用GetOrAdd 的各种重载来添加新值。这可能是default,例如NULL0 但你也可以提供一个 lambda 来为你构造一个对象,你可以使用任何类型的构造函数参数。

var x = new Dictionary<string, int>();
var val = x.GetOrAdd("MyKey", (dict, key) => dict.Count + 2);
var val2 = x.GetOrAdd("MyKey", () => Convert.ToInt32("2"));
var val3 = x.GetOrAdd("MyKey", 1);
    /// <summary>
    /// Extensions for dealing with <see cref="Dictionary{TKey,TValue}"/>
    /// </summary>
    public static class DictionaryExtensions
    {
        public static TValue GetOrAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue defaultValue = default) 
            where TValue : new() 
            => dict.GetOrAdd(key, (values, innerKey) => EqualityComparer<TValue>.Default.Equals(default(TValue), defaultValue) ? new TValue() : defaultValue);

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue defaultValue = default)
            => dict.GetOrAdd(key, (values, innerKey) => defaultValue);

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TValue> valueProvider)
            => dict.GetOrAdd(key, (values, innerKey) => valueProvider());

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> valueProvider)
            => dict.GetOrAdd(key, (values, innerKey) => valueProvider(key));

        public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<IDictionary<TKey, TValue>, TKey, TValue> valueProvider)
        {
            if (dict == null) throw new ArgumentNullException(nameof(dict));
            if (key == null) throw new ArgumentNullException(nameof(key));
            if (valueProvider == null) throw new ArgumentNullException(nameof(valueProvider));

            if (dict.TryGetValue(key, out var foundValue))
                return foundValue;

            dict[key] = valueProvider(dict, key);
            return dict[key];
        }
    }

【讨论】:

  • 如果我正确理解了您的意图,GetOrAddNew 中存在一个错误:defaultValue 可能应该与default(TValue) 进行比较。目前,my_dict.GetOrAddNew(my_key, null); 将 null 值添加到字典中,而不是调用 new TValue
  • MSDN says: Equals(Object) compares specified object to the current object。我很好奇EqualityComparer&lt;TValue&gt;.Default.Equals(default(TValue)) 中的current object 是什么?为什么会编译?
  • @4LegsDrivenCat 你说得对,我忘了在 eq 中添加 defaultValue。比较。我做了一个 dotnetfidlde 来检查这个; dotnetfiddle.net/qEkggh我会更新答案。至于它为什么编译,我必须检查一下。
  • @4LegsDrivenCat 它编译得很好,因为这是调用的 equals 代码:public virtual bool Equals(object obj) { return RuntimeHelpers.Equals(this, obj); } 在这种情况下,this 实际上是相等比较器本身(object.equals 被称为 -.-)所以它很假。
【解决方案4】:

如果构造函数需要参数,这是一个解决方案。

public static TValue GetOrCreate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TValue> createNew)
    {
        if (!dict.TryGetValue(key, out var val))
        {
            val = createNew();
            dict.Add(key, val);
        }

        return val;
    }

使用简单:

MyDict.GetOrCreate(si.Id, createNew: () => new ObjectKnowingItsId(si.Id))

【讨论】:

    【解决方案5】:

    如果你使用.Net Core,你可以使用Dictionary&lt;&gt;.TryAdd()

    var dict = new Dictionary<string, string>();
    dict.TryAdd("foo", "bar"); // returns bool whether it added or not feel free to ignore.
    var myValue = dict["foo"];
    

    【讨论】:

      【解决方案6】:

      好的,不同的方法:

      public static bool TryAddValue<TKey,TValue>(this System.Collections.Generic.IDictionary<TKey,List<TValue>> dictionary, TKey key, TValue value)
          {
              // Null check (useful or not, depending on your null checking approach)
              if (value == null)
                  return false;
      
              List<TValue> tempValue = default(List<TValue>);
      
              try
              {
                  if (!dictionary.TryGetValue(key, out tempValue))
                  {
                      dictionary.Add(key, tempValue = new List<TValue>());
                  }
                  else
                  {
                      // Double null check (useful or not, depending on your null checking approach)
                      if (tempValue == null)
                      {
                          dictionary[key] = (tempValue = new List<TValue>());
                      }
                  }
      
                  tempValue.Add(value);
                  return true;
              }
              catch
              {
                  return false;
              }
          }
      

      通过这种方式,您必须“尝试添加”您的值到通用列表(显然可以推广到通用集合)、空检查并尝试获取字典中的现有键/值。 用法和例子:

      var x = new Dictionary<string,List<string>>();
      x.TryAddValue("test", null); // return false due to null value. Doesn't add the key
      x.TryAddValue("test", "ok"); // it works adding the key/value
      x.TryAddValue("test", "ok again"); // it works adding the value to the existing list
      

      希望对你有帮助。

      【讨论】:

      • 为什么你的代码中有一个try catch?会抛出什么异常?
      【解决方案7】:

      那么这个呢?

      var keyValues = dictionary[key] = dictionary.ContainsKey(key) ? dictionary[key] : new List<string>();
      keyValues.Add(aValueForKey);
      

      【讨论】:

      • 一个。那是相当迟钝的b。如果你到处重复,那很快就会变老
      【解决方案8】:

      我缺少 Dictionary 的 GetOrAdd,而 ConcurrentDictionary 确实存在。

      ConcurrentDictionary<int,Guid> Conversion = new ConcurrentDictionary<int, Guid>();
      List<int> before = new List<int> { 1, 2, 1, 3 };
      List<Guid> after = before.Select(x => Conversion.GetOrAdd(x, Guid.NewGuid())).ToList();
      

      此代码将为每个号码生成 Guid。最终将 before 中的两个 1 转换为相同的 Guid。

      在你的情况下:

      ConcurrentDictionary<int, List<String>> dict;
      keyValues = dict.GetOrAdd(key, new List<String>());
      keyValues.Add(aValueForKey);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多