Слияние словарей в C#

каков наилучший способ объединить 2 или более словарей (Dictionary<T1,T2>) в C#? (3.0 функции, такие как LINQ, прекрасны).

Я думаю о сигнатуре метода по строкам:

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

или

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

EDIT: есть классные решения от JaredPar и Джон Скит, но я думал о чем-то, что обрабатывает дубликаты ключей. В случае столкновения не имеет значения, какое значение сохраняется в dict, если оно согласовано.

20 ответов


Это частично зависит от того, что вы хотите произойти, если вы столкнетесь с дубликатами. Например, вы могли бы сделать:

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

это взорвется, если вы получите дубликаты ключей.

EDIT: если вы используете ToLookup, вы получите поиск, который может иметь несколько значений на ключ. Вы мог бы затем преобразуйте это в словарь:

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

Это немного некрасиво и неэффективно - но это самый быстрый способ сделать это с точки зрения кода. (Я не тестировал его, по общему признанию.)


Я бы сделал так:

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

просто и легко. Согласно этот блог это даже быстрее, чем большинство циклов, поскольку его базовая реализация обращается к элементам по индексу, а не перечислителю (см. Этот ответ).

это, конечно, вызовет исключение, если есть дубликаты, поэтому вам придется проверить перед слиянием.


Ну, я опаздываю на вечеринку, но вот что я использую. Он не взрывается, если есть несколько ключей (клавиши" righter "заменяют клавиши "lefter"), может объединить несколько словарей (при желании) и сохранить тип (с ограничением, что он требует значимого открытого конструктора по умолчанию):

public static class DictionaryExtensions
{
    // Works in C#3/VS2008:
    // Returns a new dictionary of this ... others merged leftward.
    // Keeps the type of 'this', which must be default-instantiable.
    // Example: 
    //   result = map.MergeLeft(other1, other2, ...)
    public static T MergeLeft<T,K,V>(this T me, params IDictionary<K,V>[] others)
        where T : IDictionary<K,V>, new()
    {
        T newMap = new T();
        foreach (IDictionary<K,V> src in
            (new List<IDictionary<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;
    }

}

тривиальным решением будет:

using System.Collections.Generic;
...
public static Dictionary<TKey, TValue>
    Merge<TKey,TValue>(IEnumerable<Dictionary<TKey, TValue>> dictionaries)
{
    var result = new Dictionary<TKey, TValue>();
    foreach (var dict in dictionaries)
        foreach (var x in dict)
            result[x.Key] = x.Value;
    return result;
}

Dictionary<String, String> allTables = new Dictionary<String, String>();
allTables = tables1.Union(tables2).ToDictionary(pair => pair.Key, pair => pair.Value);

попробуйте следующее

static Dictionary<TKey, TValue>
    Merge<TKey, TValue>(this IEnumerable<Dictionary<TKey, TValue>> enumerable)
{
    return enumerable.SelectMany(x => x).ToDictionary(x => x.Key, y => y.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]);
}

вот вспомогательная функция, которую я использую:

using System.Collections.Generic;
namespace HelperMethods
{
    public static class MergeDictionaries
    {
        public static void Merge<TKey, TValue>(this IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second)
        {
            if (second == null || first == null) return;
            foreach (var item in second) 
                if (!first.ContainsKey(item.Key)) 
                    first.Add(item.Key, item.Value);
        }
    }
}

как насчет добавления params перегрузка?

кроме того, вы должны ввести их как IDictionary для максимальной гибкости.

public static IDictionary<TKey, TValue> Merge<TKey, TValue>(IEnumerable<IDictionary<TKey, TValue>> dictionaries)
{
    // ...
}

public static IDictionary<TKey, TValue> Merge<TKey, TValue>(params IDictionary<TKey, TValue>[] dictionaries)
{
    return Merge((IEnumerable<TKey, TValue>) dictionaries);
}

Я очень опаздываю на вечеринку и, возможно, что-то пропустил, но если либо нет дубликатов ключей, либо, как говорит OP, "в случае столкновения не имеет значения, какое значение сохраняется в dict, если оно согласовано", что не так с этим (слияние D2 в D1)?

foreach (KeyValuePair<string,int> item in D2)
            {
                 D1[item.Key] = item.Value;
            }

это кажется достаточно простым, может быть, слишком простым, я задаюсь вопросом, не упускаю ли я что-то. Это то, что я использую в некотором коде, где я знаю, что нет дубликатов ключей. Я все еще в тестировании, хотя, поэтому я хотел бы знать сейчас, если я что-то упускаю, вместо того, чтобы узнать позже.


учитывая производительность поиска и удаления словарных ключей поскольку они являются хэш-операциями, и учитывая формулировку вопроса, было лучшие путь, я думаю, что ниже совершенно правильный подход, а другие немного сверхсложны, ИМХО.

    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.


на основе ответов выше, но добавление Func-параметра, чтобы позволить вызывающему абоненту обрабатывать дубликаты:

public static Dictionary<TKey, TValue> Merge<TKey, TValue>(this IEnumerable<Dictionary<TKey, TValue>> dicts, 
                                                           Func<IGrouping<TKey, TValue>, TValue> resolveDuplicates)
{
    if (resolveDuplicates == null)
        resolveDuplicates = new Func<IGrouping<TKey, TValue>, TValue>(group => group.First());

    return dicts.SelectMany<Dictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(dict => dict)
                .ToLookup(pair => pair.Key, pair => pair.Value)
                .ToDictionary(group => group.Key, group => resolveDuplicates(group));
}

партия в значительной степени мертва сейчас, но вот "улучшенная" версия user166390, которая пробралась в мою библиотеку расширений. Помимо некоторых деталей, я добавил делегат для вычисления объединенного значения.

/// <summary>
/// Merges a dictionary against an array of other dictionaries.
/// </summary>
/// <typeparam name="TResult">The type of the resulting dictionary.</typeparam>
/// <typeparam name="TKey">The type of the key in the resulting dictionary.</typeparam>
/// <typeparam name="TValue">The type of the value in the resulting dictionary.</typeparam>
/// <param name="source">The source dictionary.</param>
/// <param name="mergeBehavior">A delegate returning the merged value. (Parameters in order: The current key, The current value, The previous value)</param>
/// <param name="mergers">Dictionaries to merge against.</param>
/// <returns>The merged dictionary.</returns>
public static TResult MergeLeft<TResult, TKey, TValue>(
    this TResult source,
    Func<TKey, TValue, TValue, TValue> mergeBehavior,
    params IDictionary<TKey, TValue>[] mergers)
    where TResult : IDictionary<TKey, TValue>, new()
{
    var result = new TResult();
    var sources = new List<IDictionary<TKey, TValue>> { source }
        .Concat(mergers);

    foreach (var kv in sources.SelectMany(src => src))
    {
        TValue previousValue;
        result.TryGetValue(kv.Key, out previousValue);
        result[kv.Key] = mergeBehavior(kv.Key, kv.Value, previousValue);
    }

    return result;
}

@Tim: должен быть комментарий, но комментарии не позволяют редактировать код.

Dictionary<string, string> t1 = new Dictionary<string, string>();
t1.Add("a", "aaa");
Dictionary<string, string> t2 = new Dictionary<string, string>();
t2.Add("b", "bee");
Dictionary<string, string> t3 = new Dictionary<string, string>();
t3.Add("c", "cee");
t3.Add("d", "dee");
t3.Add("b", "bee");
Dictionary<string, string> merged = t1.MergeLeft(t2, t2, t3);

Примечание: я применил модификацию @ANeves к решению @Andrew Orsich, поэтому MergeLeft выглядит так:

public static Dictionary<K, V> MergeLeft<K, V>(this Dictionary<K, V> me, params IDictionary<K, V>[] others)
    {
        var newMap = new Dictionary<K, V>(me, me.Comparer);
        foreach (IDictionary<K, V> src in
            (new List<IDictionary<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;
    }

Я знаю, что это старый вопрос, но поскольку у нас теперь есть LINQ, вы можете сделать это в одной строке, как это

Dictionary<T1,T2> merged;
Dictionary<T1,T2> mergee;
mergee.ToList().ForEach(kvp => merged.Add(kvp.Key, kvp.Value));

или

mergee.ToList().ForEach(kvp => merged.Append(kvp));

слияние с использованием метода расширения. Он не выдает исключение, когда есть дубликаты ключей, но заменяет эти ключи ключами из второго словаря.

internal static class DictionaryExtensions
{
    public static Dictionary<T1, T2> Merge<T1, T2>(this Dictionary<T1, T2> first, Dictionary<T1, T2> second)
    {
        if (first == null) throw new ArgumentNullException("first");
        if (second == null) throw new ArgumentNullException("second");

        var merged = new Dictionary<T1, T2>();
        first.ToList().ForEach(kv => merged[kv.Key] = kv.Value);
        second.ToList().ForEach(kv => merged[kv.Key] = kv.Value);

        return merged;
    }
}

использование:

Dictionary<string, string> merged = first.Merge(second);

слияние с помощью EqualityComparer это сопоставляет элементы для сравнения с другим значением / типом. Здесь мы будем карту из KeyValuePair (тип элемента при перечислении словаря) в Key.

public class MappedEqualityComparer<T,U> : EqualityComparer<T>
{
    Func<T,U> _map;

    public MappedEqualityComparer(Func<T,U> map)
    {
        _map = map;
    }

    public override bool Equals(T x, T y)
    {
        return EqualityComparer<U>.Default.Equals(_map(x), _map(y));
    }

    public override int GetHashCode(T obj)
    {
        return _map(obj).GetHashCode();
    }
}

использование:

// if dictA and dictB are of type Dictionary<int,string>
var dict = dictA.Concat(dictB)
                .Distinct(new MappedEqualityComparer<KeyValuePair<int,string>,int>(item => item.Key))
                .ToDictionary(item => item.Key, item=> item.Value);

или :

public static IDictionary<TKey, TValue> Merge<TKey, TValue>( IDictionary<TKey, TValue> x, IDictionary<TKey, TValue> y)
    {
        return x
            .Except(x.Join(y, z => z.Key, z => z.Key, (a, b) => a))
            .Concat(y)
            .ToDictionary(z => z.Key, z => z.Value);
    }

результатом является объединение, где для повторяющихся записей "y" выигрывает.


испугался увидеть сложные ответы, будучи новым для C#.

вот несколько простых ответов.
Слияние d1, d2 и так далее.. словари и обрабатывать любые перекрывающиеся ключи ("b" в приведенных ниже примерах):

Пример 1

{
    // 2 dictionaries,  "b" key is common with different values

    var d1 = new Dictionary<string, int>() { { "a", 10 }, { "b", 21 } };
    var d2 = new Dictionary<string, int>() { { "c", 30 }, { "b", 22 } };

    var result1 = d1.Concat(d2).GroupBy(ele => ele.Key).ToDictionary(ele => ele.Key, ele => ele.First().Value);
    // result1 is  a=10, b=21, c=30    That is, took the "b" value of the first dictionary

    var result2 = d1.Concat(d2).GroupBy(ele => ele.Key).ToDictionary(ele => ele.Key, ele => ele.Last().Value);
    // result2 is  a=10, b=22, c=30    That is, took the "b" value of the last dictionary
}

Пример 2

{
    // 3 dictionaries,  "b" key is common with different values

    var d1 = new Dictionary<string, int>() { { "a", 10 }, { "b", 21 } };
    var d2 = new Dictionary<string, int>() { { "c", 30 }, { "b", 22 } };
    var d3 = new Dictionary<string, int>() { { "d", 40 }, { "b", 23 } };

    var result1 = d1.Concat(d2).Concat(d3).GroupBy(ele => ele.Key).ToDictionary(ele => ele.Key, ele => ele.First().Value);
    // result1 is  a=10, b=21, c=30, d=40    That is, took the "b" value of the first dictionary

    var result2 = d1.Concat(d2).Concat(d3).GroupBy(ele => ele.Key).ToDictionary(ele => ele.Key, ele => ele.Last().Value);
    // result2 is  a=10, b=23, c=30, d=40    That is, took the "b" value of the last dictionary
}

для более сложных сценариев см. другие ответы.
Надеюсь, это помогло.


using System.Collections.Generic;
using System.Linq;

public static class DictionaryExtensions
{
    public enum MergeKind { SkipDuplicates, OverwriteDuplicates }
    public static void Merge<K, V>(this IDictionary<K, V> target, IDictionary<K, V> source, MergeKind kind = MergeKind.SkipDuplicates) =>
        source.ToList().ForEach(_ => { if (kind == MergeKind.OverwriteDuplicates || !target.ContainsKey(_.Key)) target[_.Key] = _.Value; });
}

вы можете пропустить / игнорировать (по умолчанию) или перезаписать дубликаты: и Ваш дядя Боб при условии, что вы не слишком суетитесь о производительности Linq, но предпочитаете вместо краткого поддерживаемого кода, как я: в этом случае вы можете удалить mergekind по умолчанию.SkipDuplicates, чтобы обеспечить выбор для вызывающего абонента и сделать разработчика осведомленным о том, каковы будут результаты!