78
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dictionaryの拡張メソッド 36選

Last updated at Posted at 2018-04-04

概要

C#のDictionaryは便利なクラスですが、メソッド関係はけっこう貧弱です。
様々なLINQ拡張メソッドが含まれているInteractive Extensions(Ix.NET)にもDictionary関係はありません。
Dictionaryに値が含まれているかどうかの条件分岐が絡むことが多いため、
ちょっとした操作で3行ぐらい消費してしまいます。

そこで定型的な処理をまとめた、C#のDictionaryをもっと使いやすくする拡張メソッドを紹介します。

デモ用コード

各拡張メソッドの説明の前に、デモで使用するコードについて説明します。
ソース配列の生成メソッドと式木を使ってDictionaryを見やすくコンソール出力するメソッドです。
(参考)式木を使った簡単なPrintデバッグ出力

/// <summary>
/// 元Dictionaryの生成
/// </summary>
private static Dictionary<string, int> CreateSourceDictionary()
    => new Dictionary<string, int>
    {
        ["A"] = 10,
        ["B"] = 20,
    };

/// <summary>
/// Dictionaryの内容をコンソール出力 ex.「sourceDict: { A = 10, B = 20, }」
/// </summary>
private static void ConsoleWriteDictionary<TKey, TValue>(Expression<Func<Dictionary<TKey, TValue>>> dictExp)
{
    var memName = (dictExp.Body as MemberExpression)?.Member.Name;
    var dict = dictExp.Compile().Invoke();
    Console.Write($"{memName}: {{ ");
    foreach (var kv in dict)
    {
        Console.Write($"{kv.Key} = {kv.Value}, ");
    }
    Console.WriteLine("}");
}

こんな感じで動きます。

デモコード
Dictionary<string, int> sourceDict = CreateSourceDictionary();
Console.WriteLine("# Start DictionaryExtension Demo");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# Start DictionaryExtension Demo
sourceDict: { A = 10, B = 20, }

Keyがないなら~する系

GetOrDefault

これを書いたことのある人は多いのではないでしょうか。
指定したKeyがDictionaryにあれば、そのValueを返し、ないならValue型のDefault値を返します。

実装
public static TValue GetOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
    => dict.TryGetValue(key, out TValue result) ? result : default(TValue);
デモコード
Console.WriteLine($"# {nameof(DictionaryExtension.GetOrDefault)}");
Console.WriteLine($"exist value = {sourceDict.GetOrDefault("A")}");
Console.WriteLine($"unexist value = {sourceDict.GetOrDefault("C")}");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# GetOrDefault
exist value = 10
unexist value = 0
sourceDict: { A = 10, B = 20, }

TryAdd

指定したKeyがDictionaryにあれば、何もせず、ないなら指定したValueと共に追加します。
追加を実行したかを返します。

実装
public static bool TryAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) where TValue : new()
    => dict.TryAdd(key, _ => new TValue());

public static bool TryAddDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
    => dict.TryAdd(key, default(TValue));

public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue addValue)
{
    bool canAdd = !dict.ContainsKey(key);

    if (canAdd)
        dict.Add(key, addValue);

    return canAdd;
}

public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> addValueFactory)
{
    bool canAdd = !dict.ContainsKey(key);

    if (canAdd)
        dict.Add(key, addValueFactory(key));

    return canAdd;
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.TryAdd)}");

Console.WriteLine($"A result = {sourceDict.TryAdd("A", 1000)}");//これのみ無視される
Console.WriteLine($"C result = {sourceDict.TryAdd("C", 3000)}");
Console.WriteLine($"D result = {sourceDict.TryAdd("D", _ => 4000)}");
Console.WriteLine($"E result = {sourceDict.TryAddNew("E")}");
Console.WriteLine($"F result = {sourceDict.TryAddDefault("F")}");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# TryAdd
A result = False
C result = True
D result = True
E result = True
F result = True
sourceDict: { A = 10, B = 20, C = 3000, D = 4000, E = 0, F = 0, }

ちょっと名前の違う2つのメソッドの違い

  • TryAddNewはValueがnew TValue()
  • TryAddDefaultはValueがdefault(TValue)

TryAddの2つのオーバーロードの使い分け

  • Valueが参照型や軽量な値型の場合は TryAdd(..., TValue addValue)
  • Valueを毎回生成したい場合や、生成処理をできるだけしたくないのであればTryAdd(..., Func<TKey, TValue> addValueFactory)

Addやインデックスでの入力との違いは指定したKeyがDictionaryにある時の挙動です。
Addで例外発生するのがいやな時に使うことが多いと思います。

種類 Keyなし Keyあり
[key] = value keyとvalueの追加 valueの上書き
Add(key, value) keyとvalueの追加 例外発生
TryAdd(key, value) keyとvalueの追加 変化なし

GetOrAdd

命名がイマイチ気に入ってない。。。
指定したKeyがDictionaryにあれば、そのValueを返し、ないなら値を追加した上で返します。
キャッシュとしてDictionaryを使うときなどに有用です。
TryAddと値の取得を組み合わせたものです。

実装
public static TValue GetOrAddNew<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) where TValue : new()
{
    dict.TryAddNew(key);
    return dict[key];
}

public static TValue GetOrAddDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
{
    dict.TryAddDefault(key);
    return dict[key];
}

public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue addValue)
{
    dict.TryAdd(key, addValue);
    return dict[key];
}

public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> valueFactory)
{
    dict.TryAdd(key, valueFactory);
    return dict[key];
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.GetOrAdd)}");
Console.WriteLine($"exist value = {sourceDict.GetOrAdd("A", 10000)}");//これのみDictionaryが変化しない
Console.WriteLine($"unexist value = {sourceDict.GetOrAdd("C", 30000)}");
Console.WriteLine($"unexist value = {sourceDict.GetOrAdd("D", _ => 40000)}");
Console.WriteLine($"unexist value = {sourceDict.GetOrAddNew("E")}");
Console.WriteLine($"unexist value = {sourceDict.GetOrAddDefault("F")}");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# GetOrAdd
exist value = 10
unexist value = 30000
unexist value = 40000
unexist value = 0
unexist value = 0
sourceDict: { A = 10, B = 20, C = 30000, D = 40000, E = 0, F = 0, }

Dictionary加工系

Add(KeyValuePair)

1つのKeyValuePairを追加します。
あるDictionaryの要素をLINQなどで加工した後に、KeyValuePairのまま、別のDictionaryに追加したい時などに使用します。

実装
public static void Add<TKey, TValue>(this IDictionary<TKey, TValue> source, KeyValuePair<TKey, TValue> addPair)
    => source.Add(addPair.Key, addPair.Value);
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.Add)}(KeyValuePair)");
sourceDict.Add(new KeyValuePair<string, int>("C", 300));
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# Add(KeyValuePair)
sourceDict: { A = 10, B = 20, C = 300, }

AddOrUpdate

指定したKeyがDictionaryにあれば、指定したデリゲートでValueを更新します、ないなら指定したValueと共にKeyを追加します。
ConcurrentDictionaryの同名のメソッドをまねたものです。

実装
public static TValue AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory)
    => dict[key] = dict.TryGetValue(key, out TValue result) ? updateValueFactory(key, result) : addValue;

public static TValue AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue addValue, Func<TValue, TValue> updateValueFactory)
    => dict[key] = dict.TryGetValue(key, out TValue result) ? updateValueFactory(result) : addValue;

public static TValue AddDefaultOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue, TValue> updateValueFactory)
    => dict.AddOrUpdate(key, default(TValue), updateValueFactory);

public static TValue AddDefaultOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TValue, TValue> updateValueFactory)
    => dict.AddOrUpdate(key, default(TValue), updateValueFactory);

public static TValue AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
    => dict[key] = dict.TryGetValue(key, out TValue result) ? updateValueFactory(key, result) : addValueFactory(key);

public static TValue AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TValue> addValueFactory, Func<TValue, TValue> updateValueFactory)
    => dict[key] = dict.TryGetValue(key, out TValue result) ? updateValueFactory(result) : addValueFactory();
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.AddOrUpdate)}");
Console.WriteLine($"A result = {sourceDict.AddOrUpdate("A", 1, (_, oldValue) => oldValue * 10)}");
Console.WriteLine($"B result = {sourceDict.AddOrUpdate("B", 2, (_, oldValue) => oldValue * 10)}");
Console.WriteLine($"B result = {sourceDict.AddOrUpdate("B", 2, (oldValue) => oldValue * 10)}");
Console.WriteLine($"C result = {sourceDict.AddOrUpdate("C", 3, (oldValue) => oldValue * 10)}");
Console.WriteLine($"D result = {sourceDict.AddDefaultOrUpdate("D", (oldValue) => oldValue + 40)}");
Console.WriteLine($"D result = {sourceDict.AddDefaultOrUpdate("D", (oldValue) => oldValue + 40)}");
Console.WriteLine($"E result = {sourceDict.AddOrUpdate("E", _ => 5, (_, oldValue) => oldValue * 10)}");
Console.WriteLine($"E result = {sourceDict.AddOrUpdate("E", () => 5, (oldValue) => oldValue * 10)}");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# AddOrUpdate
A result = 100
B result = 200
B result = 2000
C result = 3
D result = 0
D result = 40
E result = 5
E result = 50
sourceDict: { A = 100, B = 2000, C = 3, D = 40, E = 50, }

AddRange

複数のKeyValuePairを追加します。
DicitionaryをLINQで加工した後などをまるごと追加したい場合に使用します。

実装
public static void AddRange<TKey, TValue>(this IDictionary<TKey, TValue> source, IEnumerable<KeyValuePair<TKey, TValue>> addPairs)
{
    foreach (var kv in addPairs)
    {
        source.Add(kv);
    }
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.AddRange)}");
var secondDict = new Dictionary<string, int>
{
    ["C"] = 300,
    ["D"] = 400,
};
sourceDict.AddRange(secondDict);
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# AddRange
sourceDict: { A = 10, B = 20, C = 300, D = 400, }

RemoveByValue

DictionaryのRemoveメソッドはKeyを指定して要素を削除します。
このメソッドはValueを指定して一致する要素をすべて削除します。

実装
public static void RemoveByValue<TKey, TValue>(this IDictionary<TKey, TValue> source, TValue value)
{
    var removeKeys = source
        .Where(x => EqualityComparer<TValue>.Default.Equals(x.Value, value))
        .Select(x => x.Key)
        .ToArray();

    foreach (var key in removeKeys)
    {
        source.Remove(key);
    }
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.RemoveByValue)}");
sourceDict.RemoveByValue(10);
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# RemoveByValue
sourceDict: { B = 20, }

TryRemove

ConcurrentDictionaryにある同名のメソッドをまねたものです。
指定したKeyを削除しようと試みます。削除に成功した場合はout 引数に削除前の値が入ります。

実装
public static bool TryRemove<TKey, TValue>(this IDictionary<TKey, TValue> source, TKey key, out TValue value)
{
    source.TryGetValue(key, out value);
    return source.Remove(key);
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.TryRemove)}");
var successRemoveA = sourceDict.TryRemove("A", out int valueA);
var successRemoveC = sourceDict.TryRemove("C", out int valueC);
Console.WriteLine($"Remove A {successRemoveA} value = {valueA}");
Console.WriteLine($"Remove C {successRemoveC} value = {valueC}");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# TryRemove
Remove A True value = 10
Remove C False value = 0
sourceDict: { B = 20, }

ReplaceKey

Valueはそのままで、Keyだけ差し替えます。

実装
public static void ReplaceKey<TKey, TValue>(this IDictionary<TKey, TValue> source, TKey previousKey, TKey newKey)
{
    var value = source[previousKey];
    source.Remove(previousKey);
    source.Add(newKey, value);
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.ReplaceKey)}");
sourceDict.ReplaceKey("A", "AAA");
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# ReplaceKey
sourceDict: { AAA = 10, B = 20, }

KeyValuePair変換系

KeyValuePair生成

keyとValueを指定してKeyValuePairを生成する方法と
TupleとValueTupleから変換するメソッドです。
直接 new KeyValuePair<...>(...とするよりも型名を書く量が減ります。

実装
public static KeyValuePair<TKey, TValue> CreateKeyValuePair<TKey, TValue>(TKey key, TValue value)
    => new KeyValuePair<TKey, TValue>(key, value);

public static KeyValuePair<TKey, TValue> ToKeyValuePair<TKey, TValue>(this Tuple<TKey, TValue> tuple)
    => CreateKeyValuePair(tuple.Item1, tuple.Item2);

public static KeyValuePair<TKey, TValue> ToKeyValuePair<TKey, TValue>(this (TKey key, TValue value) tuple)
    => CreateKeyValuePair(tuple.key, tuple.value);
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.CreateKeyValuePair)}, Tuple {nameof(DictionaryExtension.ToKeyValuePair)}");
sourceDict.AddRange(new KeyValuePair<string, int>[]
{
    DictionaryExtension.CreateKeyValuePair("C", 300),
    ("D", 400).ToKeyValuePair(),
    Tuple.Create("E",500).ToKeyValuePair(),
});
ConsoleWriteDictionary(() => sourceDict);
デモ実行結果
# CreateKeyValuePair, Tuple ToKeyValuePair
sourceDict: { A = 10, B = 20, C = 300, D = 400, E = 500, }

ToTuple

こんどは逆方向のKeyValuePairからTupleとValueTupleに変換します。

実装
public static Tuple<TKey, TValue> ToTuple<TKey, TValue>(this KeyValuePair<TKey, TValue> kv)
    => new Tuple<TKey, TValue>(kv.Key, kv.Value);

public static (TKey key, TValue value) ToValueTuple<TKey, TValue>(this KeyValuePair<TKey, TValue> kv)
    => (kv.Key, kv.Value);
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# KeyValuePair {nameof(DictionaryExtension.ToTuple)}");
Console.WriteLine($"Tuple = {sourceDict.First().ToTuple()}");
Console.WriteLine($"ValueTuple = {sourceDict.First().ToValueTuple()}");
デモ実行結果
# KeyValuePair ToTuple
Tuple = (A, 10)
ValueTuple = (A, 10)

Dictionary変換系

ToDictionary

Dictionaryを複製(ディープコピー)したい時やLINQで加工した後に再度Dictionaryにしたい時に使います。

実装
public static Dictionary<Tkey, TValue> ToDictionary<Tkey, TValue>(this IEnumerable<KeyValuePair<Tkey, TValue>> source)
    => source.ToDictionary(
        keySelector: kv => kv.Key,
        elementSelector: kv => kv.Value);
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# ToDictionary (Copy)");
//Dictionaryを別のDictionaryに変換(値コピー)
var copiedDict = sourceDict.ToDictionary();
//元のDictionaryを変更しても、コピー先には反映されない
copiedDict["A"] = 100;
//LINQで加工したものもDictionaryにできる
var filterdDict = sourceDict.Where(x => x.Key == "A").ToDictionary();

Console.WriteLine("(after copy and change )");
ConsoleWriteDictionary(() => copiedDict);
ConsoleWriteDictionary(() => sourceDict);
ConsoleWriteDictionary(() => filterdDict);
デモ実行結果
# ToDictionary (Copy)
(after copy and change )
copiedDict: { A = 100, B = 20, }
sourceDict: { A = 10, B = 20, }
filterdDict: { A = 10, }

SelectValue

Keyはそのままで、ValueのみSelectで加工したい時に使用します。
通常のSelectと同じでIndex付きのオーバーロードもあります。
最後に前述のToDictionary()メソッドを使うことで直接Dictionaryに戻せます。

実装
public static IEnumerable<KeyValuePair<Tkey, TValueResult>> SelectValue<Tkey, TValueSource, TValueResult>(this IEnumerable<KeyValuePair<Tkey, TValueSource>> source, Func<TValueSource, TValueResult> valueSelector)
    => source.Select(kv => new KeyValuePair<Tkey, TValueResult>(kv.Key, valueSelector(kv.Value)));

public static IEnumerable<KeyValuePair<Tkey, TValueResult>> SelectValue<Tkey, TValueSource, TValueResult>(this IEnumerable<KeyValuePair<Tkey, TValueSource>> source, Func<TValueSource, int, TValueResult> valueSelector)
    => source.Select((kv, index) => new KeyValuePair<Tkey, TValueResult>(kv.Key, valueSelector(kv.Value, index)));
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.SelectValue)}");
var valueChangedDict = sourceDict.SelectValue((x, i) => x * 0.01 + i).ToDictionary();
ConsoleWriteDictionary(() => valueChangedDict);
デモ実行結果
# SelectValue
convertedDict: { A = 0.1, B = 1.2, }

SelectKey

SelectValueのKey版です。

実装
public static IEnumerable<KeyValuePair<TKeyResult, TValue>> SelectKey<TKeySource, TValue, TKeyResult>(
    this IEnumerable<KeyValuePair<TKeySource, TValue>> source,
    Func<TKeySource, TKeyResult> keySelector)
    => source.Select(kv => new KeyValuePair<TKeyResult, TValue>(keySelector(kv.Key), kv.Value));

public static IEnumerable<KeyValuePair<TKeyResult, TValue>> SelectKey<TKeySource, TValue, TKeyResult>(
    this IEnumerable<KeyValuePair<TKeySource, TValue>> source,
    Func<TKeySource, int, TKeyResult> keySelector)
    => source.Select((kv, index) => new KeyValuePair<TKeyResult, TValue>(keySelector(kv.Key, index), kv.Value));
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.SelectKey)}");
var keyChangedDict = sourceDict.SelectKey((x, i) => $"{x}-{i}").ToDictionary();
ConsoleWriteDictionary(() => keyChangedDict);
デモ実行結果
# SelectKey
keyChangedDict: { A-0 = 10, B-1 = 20, }

FlipKeyValue

KeyとValueを入れ替えたDictionaryを返します。
デモにもあるように、Valueが重複していると例外が発生します。

実装
public static Dictionary<TValueToKey, TkeyToValue> FlipKeyValue<TkeyToValue, TValueToKey>(this IEnumerable<KeyValuePair<TkeyToValue, TValueToKey>> source)
    => source.ToDictionary(
        keySelector: kv => kv.Value,
        elementSelector: kv => kv.Key);
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.FlipKeyValue)}");
var flipedDict = sourceDict.FlipKeyValue();
ConsoleWriteDictionary(() => flipedDict);
//実行時エラー 複数のKeyに同じValueがあった場合、入れ替えるとKeyが重複してしまう
//sourceDict["B"] = 10;
//flipedDict = sourceDict.FlipKeyValue();
デモ実行結果
# FlipKeyValue
flipedDict: { 10 = A, 20 = B, }

DistinctByKey

LINQで加工した後に、Keyが重複した時などはそのままDictionaryにするとKeyの重複で例外が発生してしまうので、
このDistinctByKeyで重複を除きます。
Keyが重複していたら、先にあるValueが使用されます。

内部でKeyの一致を判定するIEqualityComparer継承クラスを定義して使用しています。

実装
public static IEnumerable<KeyValuePair<TKey, TValue>> DistinctByKey<TKey, TValue>(
    this IEnumerable<KeyValuePair<TKey, TValue>> source)
    => source.Distinct(OnlyKeyComparer<TKey, TValue>.Instance);

public class OnlyKeyComparer<Tkey, TValue> : IEqualityComparer<KeyValuePair<Tkey, TValue>>
{
    public static OnlyKeyComparer<Tkey, TValue> Instance { get; } = new OnlyKeyComparer<Tkey, TValue>();

    public bool Equals(KeyValuePair<Tkey, TValue> x, KeyValuePair<Tkey, TValue> y)
        => EqualityComparer<Tkey>.Default.Equals(x.Key, y.Key);

    public int GetHashCode(KeyValuePair<Tkey, TValue> obj) => obj.Key.GetHashCode();
}
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.DistinctByKey)}");
var distinctDict = sourceDict.SelectKey(_ => "X").DistinctByKey().ToDictionary();
ConsoleWriteDictionary(() => distinctDict);
デモ実行結果
# DistinctByKey
distinctDict: { X = 10, }

Merge

2つのDictionaryを合流させた1つのDictionaryを返します。

オーバーロード2つのうち、引数が少ない方はKeyが重複していたら、先のDictionaryのValueが使用されます。
もう1つは重複した場合の挙動を細かく制御するためのデリゲートを受け取ります。

実装
public static IEnumerable<KeyValuePair<TKey, TValue>> Merge<TKey, TValue>(
    this IEnumerable<KeyValuePair<TKey, TValue>> firstDict,
    IEnumerable<KeyValuePair<TKey, TValue>> secondDict)
    => firstDict.Concat(secondDict).DistinctByKey();

public static IEnumerable<KeyValuePair<TKey, TValue>> Merge<TKey, TValue>(
    this IEnumerable<KeyValuePair<TKey, TValue>> firstDict,
    IEnumerable<KeyValuePair<TKey, TValue>> secondDict,
    Func<TKey, TValue, TValue, TValue> mergerIfDoubled)
    => firstDict.Concat(secondDict)
        .ToLookup(kv => kv.Key)
        .Select(kVs => kVs.Count() <= 1 ? kVs.First() :
            new KeyValuePair<TKey, TValue>(
                kVs.Key,
                mergerIfDoubled(kVs.Key, kVs.First().Value, kVs.Last().Value)));
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.Merge)}");
var secondDict = new Dictionary<string, int>
{
    ["D"] = 400,
    ["B"] = 200,
};
ConsoleWriteDictionary(() => secondDict);
//2つのDictionaryを合流、Keyが重複していたら、先のDictionaryのValueが使用される
var mergedDict = sourceDict.Merge(secondDict).ToDictionary();
var mergedPlusedDict = sourceDict.Merge(secondDict, (k, f, s) => k.Length + f + s).ToDictionary();
ConsoleWriteDictionary(() => mergedDict);
ConsoleWriteDictionary(() => mergedPlusedDict);
デモ実行結果
# Merge
secondDict: { D = 400, B = 200, }
mergedDict: { A = 10, B = 20, D = 400, }
mergedPlusedDict: { A = 10, B = 221, D = 400, }

JoinByKey

LINQの中でも強力そうだけど、シグネチャがいかつくて、とっつきづらいJoinのKeySelctorをDictionaryのKeyに固定したものです。
Mergeは基本的に重複がない2つの同型のDictionary(KeyValuePairコレクション)を1つに加工するものです。
JoinByKeyは2つのDictionaryからKeyが共通な要素を抜き出し、1つに加工するものです。
Mergeと違い2つの入力と出力のValueの型は同じである必要がありません。

実装
public static IEnumerable<KeyValuePair<TKey, TValueResult>> JoinByKey<TKey, TValueFirst, TValueSecond, TValueResult>(
    this IEnumerable<KeyValuePair<TKey, TValueFirst>> firstDict,
    IEnumerable<KeyValuePair<TKey, TValueSecond>> secondDict,
    Func<TKey, TValueFirst, TValueSecond, TValueResult> resultSelector)
    => firstDict.Join(secondDict,
                fKV => fKV.Key,
                sKV => sKV.Key,
                (f, s) => new KeyValuePair<TKey, TValueResult>(f.Key, resultSelector(f.Key, f.Value, s.Value)));
デモコード
sourceDict = CreateSourceDictionary();
Console.WriteLine($"# {nameof(DictionaryExtension.JoinByKey)}");
var secondDict = new Dictionary<string, bool>
{
    ["B"] = true,
    ["D"] = false,
};
ConsoleWriteDictionary(() => secondDict);
var joinedDict = sourceDict.JoinByKey(secondDict, (k, f, s) => k.Length + f + (s ? 100 : -100)).ToDictionary();
ConsoleWriteDictionary(() => joinedDict);
デモ実行結果
# JoinByKey
secondDict: { B = True, D = False, }
joinedDict: { B = 121, }

注意点

多くのメソッドで汎用性を持たせるためにIDictionary<TKey, TValue>を使用していますが、パフォーマンス面ではDictionary<TKey, TValue>を使用したほうが良いです。

参考:http://www.nimaara.com/2016/03/06/beware-of-the-idictionary-tkey-tvalue/

あとがき

こんなに長くなる予定じゃなかった。
こんなのもあるかも、これがあるならあれも、とかやってたらこんな量に。

環境

VisualStudio2017
.NET Framework 4.7
C#7

78
79
12

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
78
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?