2016-03-04 4 views
1

У меня есть два словаря в C# 6.0, и я бы хотел объединить их в умном стиле.Объединить два словаря <string, string> асимметричным образом

Возьмите первый словарь foo как:

var foo = new Dictionary<string, string> 
{ 
    {"a", "10"}, 
    {"b", "20"}, 
    {"c", "30"}, 
}; 

И второй словарь bar как:

var bar = new Dictionary<string, string> 
{ 
    {"a", "333"}, 
    {"e", "444"}, 
    {"f", "555"}, 
}; 

я хотел бы объединить их в одном словаре с этой логикой:

  • Если ключ в foo, но нет т в bar игнорировать его в новом словаре
  • Если ключ в bar но не в foo, принять его в новом словаре
  • если ключ находится в как foo и bar, принимает значение из foo в новом словаре

Вот мой ожидаемый результат:

var result = new Dictionary<string, string> 
{ 
    {"a", "10"}, //this comes from foo 
    {"e", "444"}, //this comes from bar 
    {"f", "555"}, //this comes from bar 
}; 

Есть ли разумный способ справиться с этой проблемой без forloop (выражения LINQ в порядке)?

+2

Честно это действительно непостижимо, почему некоторые люди боятся петли *, а также, почему другие люди страх LINQ * –

ответ

1

Вы можете использовать HashSet<T> методы и LINQ:

1)

var fooKeys = new HashSet<string>(foo.Keys); 
var barKeys = new HashSet<string>(bar.Keys); 
fooKeys.IntersectWith(barKeys); // remove all from fooKeys which are not in both 
barKeys.ExceptWith(fooKeys); // remove all from barKeys which are remaining in fooKeys and also in barKeys 
Dictionary<string, string> result = fooKeys 
    .Select(fooKey => new KeyValuePair<string, string>(fooKey, foo[fooKey])) 
    .Concat(barKeys.Select(bKey => new KeyValuePair<string, string>(bKey, bar[bKey]))) 
    .ToDictionary(kv => kv.Key, kv => kv.Value); 

Это безопасно, потому что оба исключают друг друга. Это также очень эффективно, так как эти методы HashSet имеют сложность O(n) с двумя наборами.


Если это не понятно, по вашему мнению, может быть, вы, как это больше:

2)

var inBoth = from kv1 in foo 
      join kv2 in bar 
      on kv1.Key equals kv2.Key 
      select kv1; 
var onlyInBar = bar.Keys.Except(foo.Keys) 
    .Select(b => new KeyValuePair<string, string>(b, bar[b])); 
Dictionary<string, string> result = new Dictionary<string, string>(); 
foreach (var kv in inBoth.Concat(onlyInBar)) 
    result.Add(kv.Key, kv.Value); 

Первый запрос использует объединение (более читаемым в запросе-синтаксис), который только возвращает пары значений ключа из первого словаря, где ключ также существует во втором словаре. Второй запрос использует Enumerable.Except, чтобы исключить все из второго словаря, которые находятся в первом. Оба, Enumerable.Join и Enumerable.Except используют комплекты под капотом, поэтому они очень эффективны.

Стоит отметить, что из-за отложенного выполнения LINQ оба запроса выполняются только на foreach (var kv in inBoth.Concat(onlyInBar)), а не раньше.


Вероятно, самый простой и самый читаемый подход, "LINQ слева внешнее_соединение":

3)

KeyValuePair<string, string> defaultPair = default(KeyValuePair<string, string>); 
var query = from barKv in bar 
      join fooKv in foo 
      on barKv.Key equals fooKv.Key into gj_bf 
      from bf in gj_bf.DefaultIfEmpty() 
      select bf.Equals(defaultPair) ? barKv : bf; 
foreach (var kv in query) 
    result.Add(kv.Key, kv.Value); 
+1

LINQ - один из моих страхов :) – Sakura

+1

Мне нравится LINQ, если я могу использовать его для написания чистого и понятного кода. если полученный код не является «легким», я предпочитаю использовать для/foreach петли. –

+0

@GianPaolo: может быть, вам нравятся мои другие подходы больше, особенно последние :) –

1

Вы можете использовать GroupJoin как это:

var res = 
    bar 
    .GroupJoin(
     foo, 
     kvp => kvp.Key, 
     kvp => kvp.Key, 
     (kvp, g) => new KeyValuePair<string, string>(kvp.Key, g.FirstOrDefault().Value ?? kvp.Value)) 
    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); 

Уловка здесь GroupJoin бар с foo! таким образом, все из бара появится в конечном результате, и для тех же ключей объединенный результат будет IEnumerable совпадающих результатов из второй коллекции, которая в вашем случае является foo, и поскольку она равна Dictionary, поэтому согласованные результаты будут содержать только один элемент и все, что вам нужно сделать, это получить его ценность. В случае отсутствия соответствия (элементы в баре, но не в foo) коллекция совпадающих результатов будет пуста, поэтому FirstOrDefault() вернет значение по умолчанию KeyValuePair<string, string> с ключом и значением, установленным как null. Таким образом, в этом случае мы просто получаем значение из нашей первой коллекции (в вашем случае).

1

А (простое) Linq решение:

var newDict = new Dictionary<string, string>(); 

    var toIncludeFromFoo = bar.Keys.Intersect(foo.Keys).ToList(); 
    toIncludeFromFoo.ForEach(x => newDict [x] = foo[x]); 

    var toAddFromBar = bar.Keys.Except(foo.Keys).ToList(); 
    toAddFromBar.ForEach(x => newDict [x] = bar[x]); 
+1

Очень чистое решение! В вашем ответе отсутствует ссылка между newDict и toAdd;) – Rowandish

+0

Код отредактирован, вы заметили ошибку! –

+1

Это не то, что он должен делать. Вы также добавляете все из 'foo'. Словарь результатов должен содержать только foo, где ключ также находится в баре. Поэтому ваш словарь содержит 5 ключевых значений. Посмотрите на желаемый результат OP и упомянутое выше требование. –

1

Ваша логика может быть упрощена следующим образом:

Результат будет содержать все ключи от bar, со значением, взятым из foo, если существует, в противном случае из bar ,

который переводит на что-то вроде этого:

var result = bar.ToDictionary(barItem => barItem.Key, barItem => 
    foo.ContainsKey(barItem.Key) ? foo[barItem.Key] : barItem.Value); 

или немного дольше, но более оптимальным:

var result = bar.ToDictionary(barItem => barItem.Key, barItem => 
    { string fooValue; return foo.TryGetValue(barItem.Key, out fooValue) ? fooValue : barItem.Value; }); 
Смежные вопросы