LoginSignup
16

More than 5 years have passed since last update.

C# の Dictionary 同士を簡単にマージする方法

Last updated at Posted at 2018-11-06

まず方法から...

C# のDictionary<TKey,TValue>同士をマージするコードを書く必要があり、どうせならfor文ぐるぐる、ContainsKeyで重複チェックして...より LINQ でスマートに書きたいと思い調べたのでまとめます。

var dic1 = new Dictionary<string, int> {
  ["oarange"] = 100,
  ["apple"] = 150,
};
var dic2 = new Dictionary<string, int> {
  ["apple"] = 180,
  ["banana"] = 200,
};

var merged = dic1.Concat(dic2)
  .GroupBy(
    pair => pair.Key,
    (_, pairs) => pairs.First()
  ).ToDictionary(
    pair => pair.Key,
    pair => pair.Value
  );

foreach (var (k, v) in merged) {
  Console.WriteLine($"key: {k}, value: {v}");
}
結果
key: oarange, value: 100
key: apple, value: 150
key: banana, value: 200

以下は上記のコードの説明です。
興味があれば読んでいただけると嬉しいです。:smiley:

追記

コメントいただいた以下の方法の方がシンプルです。

var merged = dic1
  .Concat(dic2.Where(pair =>
    !dic1.ContainsKey(pair.Key))
  ).ToDictionary(
    pair => pair.Key,
    pair => pair.Value
  );

説明

型を明示すると以下のようになります。

// マージする Dictionary
var dict1 = new Dictionary<string, int> {
  ["oarange"] = 100,
  ["apple"] = 150,
};
var dict2 = new Dictionary<string, int> {
  ["apple"] = 180,
  ["banana"] = 200,
};

// Dictionary 同士を連結する
IEnumerable<KeyValuePair<string, int>> concated = dict1.Concat(dict2);

// キーが重複する KeyValuePair を削除する
IEnumerable<KeyValuePair<string, int>> pairs = concated.GroupBy(
  (KeyValuePair<string, int> pair) => pair.Key,
  (string key, IEnumerable<KeyValuePair<string, int>> keyValues) => keyValues.First()
);

// KeyValuePair のシーケンスを Dictionary 化する
Dictionary<string, int> mergedDict = pairs.ToDictionary(
  (KeyValuePair<string, int> pair) => pair.Key,
  (KeyValuePair<string, int> pair) => pair.Value
);

Dictionary同士を連結

IEnumerable<KeyValuePair<string, int>> concated = dict1.Concat(dict2);

ConcatでなくUnionでも連結できます。
ただしUnionの重複判定はキーでなくGetHashCodeで得られれるハッシュ値とEqualsで行われます。
つまり、和集合にはならずConcatと同様にToDictionaryの前に重複を弾く必要があります。
Unionの第二引数にIEqualityComparer<Dictionary<string, int>を実装したクラスを渡せば重複を弾けますが面倒なのでやりません。

キーが重複するKeyValuePairの削除

IEnumerable<KeyValuePair<string, int>> pairs = concated.GroupBy(
  (KeyValuePair<string, int> pair) => pair.Key,
  (string key, IEnumerable<KeyValuePair<string, int>> keyValues) => keyValues.First()
);

ToDictionaryはキーが重複すると例外をスローするのでキーが重複するKeyValuePairを取り除く必要があります。
GroupByの第二引数にFunc<TKey,IEnumerable<TElement>,TResult>を渡すと各グループから結果値を作成することができます。
KeyValuePairを返しているため結果がIEnumerable<IGrouping<string, int>>でなくIEnumerable<KeyValuePair<string, int>>になっています。

Dictionary同士のキーが重複する場合、どちらを優先するか

keyValues.First()ではdict1が優先されkeyValues.Last()にするとdict2の値で上書きできます。

concated.GroupBy(
  pair => pair.Key,
  keyValues => keyValues.Last() // First -> Last
);
var dict1 = new Dictionary<string, int> {
  ["oarange"] = 100,
  ["apple"] = 150, // <- 重複
};
var dict2 = new Dictionary<string, int> {
  ["apple"] = 180, // <- 重複
  ["banana"] = 200,
};
keyValues.Firstにした場合の結果
key: oarange, value: 100
key: apple, value: 150  <- dict1 が優先
key: banana, value: 200
keyValues.Lastにした場合の結果
key: oarange, value: 100
key: apple, value: 180  <- dict2 が優先
key: banana, value: 200

KeyValuePairのシーケンスをDictionary

Dictionary<string, int> mergedDict = pairs.ToDictionary(
  (KeyValuePair<string, int> pair) => pair.Key,
  (KeyValuePair<string, int> pair) => pair.Value
);

ToDictionaryは第一引数でキーを、第二引数にバリューをそれぞれラムダ式で指定できます。

拡張メソッド

for文回して~ContainsKeyで重複判定して~と比べるとConcat > GroupBy > ToDicitonaryは短くなりますが、何度も書くのは面倒なので拡張メソッドを定義しておきます。

public static class IDictionaryExtentions {
  public static IDictionary<TKey, TValue> Merge<TKey, TValue>(
    this IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second
  ) => first.Concat(second)
    .GroupBy(pair => pair.Key, (_, pairs) => pairs.First())
    .ToDictionary(pair => pair.Key, pair => pair.Value);

  public static IDictionary<TKey, TValue> Update<TKey, TValue>(
    this IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second
  ) => first.Concat(second)
    .GroupBy(pair => pair.Key, (_, pairs) => pairs.Last())
    .ToDictionary(pair => pair.Key, pair => pair.Value);
}

Mergeは重複するキーがある場合、呼出し側のDictionaryを優先します。
Updateは重複キーがある場合、引数で与えたDictionaryを優先します。

var dic1 = new Dictionary<string, int> {
  ["oarange"] = 100,
  ["apple"] = 150,
};
var dic2 = new Dictionary<string, int> {
  ["apple"] = 180,
  ["banana"] = 200,
};

var merged = dict1.Merge(dict2);
foreach (var (k, v) in merged) {
  Console.WriteLine($"key: {k}, value: {v}");
}

/*
  key: oarange, value: 100
  key: apple, value: 150  <- 上書きされない
  key: banana, value: 200
 */

var updated = dict1.Update(dict2);
foreach (var (k, v) in updated) {
  Console.WriteLine($"key: {k}, value: {v}");
}

/*
  key: oarange, value: 100
  key: apple, value: 180  <- 上書きする
  key: banana, value: 200
 */

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16