在c#中合并2个或更多字典(Dictionary<TKey, TValue>)的最佳方法是什么? (像LINQ这样的3.0特性就可以了)。

我正在考虑一个方法签名,如下所示:

public static Dictionary<TKey,TValue>
                 Merge<TKey,TValue>(Dictionary<TKey,TValue>[] dictionaries);

or

public static Dictionary<TKey,TValue>
                 Merge<TKey,TValue>(IEnumerable<Dictionary<TKey,TValue>> dictionaries);

关于重复键的处理:在发生冲突的情况下,保存到字典中的值并不重要,只要它是一致的。


当前回答

我会这样做:

dictionaryFrom.ToList().ForEach(x => dictionaryTo.Add(x.Key, x.Value));

简单易行。根据这篇博客文章,它甚至比大多数循环更快,因为它的底层实现通过索引而不是枚举来访问元素(参见这个答案)。

如果存在重复,它当然会抛出异常,因此您必须在合并之前进行检查。

其他回答

下面的方法对我有用。如果存在重复项,则使用dictA的值。

public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> dictA, IDictionary<TKey, TValue> dictB)
    where TValue : class
{
    return dictA.Keys.Union(dictB.Keys).ToDictionary(k => k, k => dictA.ContainsKey(k) ? dictA[k] : dictB[k]);
}

根据这篇文章中所有的答案,这里是我能想到的最通用的解决方案。

我创建了两个版本的IDictionary.Merge()扩展:

<T, U>(sourceLeft, sourceRight) <T, U>(sourceLeft, sourceRight, Func<U, U, U> mergeExpression)

其中第二个是第一个的修改版本,允许你指定一个lambda表达式来处理像这样的重复:

Dictionary<string, object> customAttributes = 
  HtmlHelper
    .AnonymousObjectToHtmlAttributes(htmlAttributes)
    .ToDictionary(
      ca => ca.Key, 
      ca => ca.Value
    );

Dictionary<string, object> fixedAttributes = 
  new RouteValueDictionary(
    new { 
      @class = "form-control"
    }).ToDictionary(
      fa => fa.Key, 
      fa => fa.Value
    );

//appending the html class attributes
IDictionary<string, object> editorAttributes = fixedAttributes.Merge(customAttributes, (leftValue, rightValue) => leftValue + " " + rightValue);

(您可以关注ToDictionary()和Merge()部分)

下面是扩展类(右边有两个版本的扩展,接受一个IDictionary的集合):

  public static class IDictionaryExtension
  {
    public static IDictionary<T, U> Merge<T, U>(this IDictionary<T, U> sourceLeft, IDictionary<T, U> sourceRight)
    {
      IDictionary<T, U> result = new Dictionary<T,U>();

      sourceLeft
        .Concat(sourceRight)
        .ToList()
        .ForEach(kvp => 
          result[kvp.Key] = kvp.Value
        );

      return result;
    }

    public static IDictionary<T, U> Merge<T, U>(this IDictionary<T, U> sourceLeft, IDictionary<T, U> sourceRight, Func<U, U, U> mergeExpression)
    {
      IDictionary<T, U> result = new Dictionary<T,U>();

      //Merge expression example
      //(leftValue, rightValue) => leftValue + " " + rightValue;

      sourceLeft
        .Concat(sourceRight)
        .ToList()
        .ForEach(kvp => 
          result[kvp.Key] =
            (!result.ContainsKey(kvp.Key))
              ? kvp.Value
              : mergeExpression(result[kvp.Key], kvp.Value)
        );

      return result;
    }


    public static IDictionary<T, U> Merge<T, U>(this IDictionary<T, U> sourceLeft, IEnumerable<IDictionary<T, U>> sourcesRight)
    {
      IDictionary<T, U> result = new Dictionary<T, U>();
      
      new[] { sourceLeft }
        .Concat(sourcesRight)
        .ToList()
        .ForEach(dic =>
          result = result.Merge(dic)
        );

      return result;
    }

    public static IDictionary<T, U> Merge<T, U>(this IDictionary<T, U> sourceLeft, IEnumerable<IDictionary<T, U>> sourcesRight, Func<U, U, U> mergeExpression)
    {
      IDictionary<T, U> result = new Dictionary<T, U>();

      new[] { sourceLeft }
        .Concat(sourcesRight)
        .ToList()
        .ForEach(dic =>
          result = result.Merge(dic, mergeExpression)
        );

      return result;
    }
  }

mergeExpression让您轻松地处理您想合并项目的方式,如加法,除法,乘法或任何您想要的特定过程。

请注意,我还没有测试扩展的集合版本…它们可能仍然需要一些调整。

此外,扩展不修改原来的字典,你必须分配回来,如果你想。

选项1:这取决于在确定两个字典中都没有重复键的情况下想要发生什么。比你能做的:

var result = dictionary1.Union(dictionary2).ToDictionary(k => k.Key, v => v.Value)

注意:如果在字典中获得任何重复的键,将抛出错误。

选项2:如果你可以有重复的键,那么你必须使用where子句来处理重复的键。

var result = dictionary1.Union(dictionary2.Where(k => !dictionary1.ContainsKey(k.Key))).ToDictionary(k => k.Key, v => v.Value)

注意:它不会得到重复的密钥。如果有任何重复的键,那么它将获得dictionary1的键。

选项3:如果你想使用ToLookup。然后您将得到一个查找,每个键可以有多个值。你可以把这个查找转换成一个字典:

var result = dictionaries.SelectMany(dict => dict)
                         .ToLookup(pair => pair.Key, pair => pair.Value)
                         .ToDictionary(group => group.Key, group => group.First());

@user166390的回答版本增加了一个IEqualityComparer参数,以允许不区分大小写的键比较。

    public static T MergeLeft<T, K, V>(this T me, params Dictionary<K, V>[] others)
        where T : Dictionary<K, V>, new()
    {
        return me.MergeLeft(me.Comparer, others);
    }

    public static T MergeLeft<T, K, V>(this T me, IEqualityComparer<K> comparer, params Dictionary<K, V>[] others)
        where T : Dictionary<K, V>, new()
    {
        T newMap = Activator.CreateInstance(typeof(T), new object[] { comparer }) as T;

        foreach (Dictionary<K, V> src in 
            (new List<Dictionary<K, V>> { me }).Concat(others))
        {
            // ^-- echk. Not quite there type-system.
            foreach (KeyValuePair<K, V> p in src)
            {
                newMap[p.Key] = p.Value;
            }
        }
        return newMap;
    }

考虑到字典键查找和删除的性能,因为它们是哈希操作,并且考虑到问题的措辞是最好的方式,我认为下面是一个完全有效的方法,而其他方法有点过于复杂,恕我冒昧。

    public static void MergeOverwrite<T1, T2>(this IDictionary<T1, T2> dictionary, IDictionary<T1, T2> newElements)
    {
        if (newElements == null) return;

        foreach (var e in newElements)
        {
            dictionary.Remove(e.Key); //or if you don't want to overwrite do (if !.Contains()
            dictionary.Add(e);
        }
    }

或者如果你在多线程应用程序中工作,你的字典无论如何都需要线程安全,你应该这样做:

    public static void MergeOverwrite<T1, T2>(this ConcurrentDictionary<T1, T2> dictionary, IDictionary<T1, T2> newElements)
    {
        if (newElements == null || newElements.Count == 0) return;

        foreach (var ne in newElements)
        {
            dictionary.AddOrUpdate(ne.Key, ne.Value, (key, value) => value);
        }
    }

然后可以对其进行包装,使其处理字典的枚举。无论如何,您看到的是~O(3n)(所有条件都是完美的),因为. add()将在幕后执行额外的、不必要的但实际上是免费的Contains()。我觉得没有比这更好的了。

如果希望限制大型集合上的额外操作,则应该将将要合并的每个字典的Count相加,并将目标字典的容量设置为该值,这样可以避免以后调整大小的成本。最终产品是这样的…

    public static IDictionary<T1, T2> MergeAllOverwrite<T1, T2>(IList<IDictionary<T1, T2>> allDictionaries)
    {
        var initSize = allDictionaries.Sum(d => d.Count);
        var resultDictionary = new Dictionary<T1, T2>(initSize);
        allDictionaries.ForEach(resultDictionary.MergeOverwrite);
        return resultDictionary;
    }

注意,我在这个方法中引入了一个IList<T>…主要是因为如果你接受一个IEnumerable<T>,你已经向同一个集合的多个枚举开放了,如果你从一个延迟的LINQ语句中获得字典集合,这可能是非常昂贵的。