ListからDictionary作る時もLINQを使おうぜ!ILookupも便利だぜ!

  • 173
    Like
  • 1
    Comment
More than 1 year has passed since last update.

はじめに

 みなさんLINQ使っていますか?LINQ最高ですよね!

 さて、ListからDictionary作るようなことをしませんか?空のDictionaryを作って、foreach文を使ってListをまわし、Dictionaryに要素を追加していってDictionaryを作るコードなどを書きませんか?

 実はLINQを使って非常に簡潔に、ListなどのクラスからDictionaryを作ることができるのです。

Dictionaryを作る時、もしかしたらこんなコード書きません?

 こんな列挙型とクラスがあります。

Element列挙型とSkillクラス
public enum Element
{
    Fire,
    Thunder,
    Wind,
}

public class Skill
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Element Element { get; set; }
}

 List<Skill>型のインスタンス、skillListがあります。ちなみにint型のプロパティIdの値はそれぞれのSkillで固有で、重複はしません。

List<Skill> skillList = new List<Skill> {
    new Skill {
        Id = 0,
        Name = "ファイアー",
        Element = Element.Fire,
    },
    new Skill {
        Id = 1,
        Name = "エルファイアー",
        Element = Element.Fire,
    },
    new Skill {
        Id = 2,
        Name = "サンダー",
        Element = Element.Thunder,
    },
    new Skill {
        Id = 3,
        Name = "サンダーストーム",
        Element = Element.Thunder,
    },
    new Skill {
        Id = 4,
        Name = "エイルカリバー",
        Element = Element.Wind,
    }
};

 さてさて、このList<Skill>のskillListから、int型のIdをキーとし、SkillをバリューとするDictionary<int, Skill>を作ります。次のようなコードです。

ListからforeachでDictionaryを作る
Dictionary<int, Skill> skillDictonary = new Dictionary<int, Skill> ();
foreach (Skill skill in skillList) {
    skillDictionary.Add (skill.Id, skill);
}

 使うとしたらこんなかんじでしょうか。

作ったDictionaryを使う
int skillId = GetTargetSkillId (); // 対象のId(int型)を取得
Skill targetSkill = skillDictionary [skillId];

 どうでしょう、このようなListからDictionaryを作るコードを書いたことはありませんか?実は、このようなコードはLINQを使えば1行で書けてしまうのです。

LINQのToDictionaryメソッドを使って1発でDictionaryを作成!

 LINQのToDictionaryメソッドを使って、さきほどのコードを書き換えます。

ToDictionaryメソッドを使って、ListからDictionarを生成
Dictionary<int, Skill> skillDictonary = skillList.ToDictionary (skill => skill.Id);

 非常に簡潔ですね。1行で書けてしまいました!これだけのコード量で、目的のDictionaryができてしまいました。

 ToDictionaryメソッドはLINQのメソッドで、List<T>型でなくても、IEnumeralble<T>型を実装したクラスのインスタンスであれば利用できます。ToDictionaryメソッドは他のLINQのメソッドと同様に、複数のオーバーロードを持っています。

 ここで使ったオーバーロードは、引数にデリゲートをとります。IEnumerable<T>の各要素がDictionaryのバリューになります。引数に渡したデリゲートを、各要素(バリュー)に適用した結果が要素(バリュー)に対応するキーとなります。

 MSDNに記載されている構文を次に示します。

ToDictionaryの構文1
public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector
)

 もし、二つの要素に対して同じキーが生成された場合、ArgumentExceptionが発生します。

こんなDictionaryもできます!

 さきほどはSkillのIdからSkillを引くDictonary<int, Skill>を作りました。次のようなIdから名前を引けるDictionary<int, string>を作ることもあるのではないでしょうか?

ListからforeachでDictionaryを作る(その2)
Dictionary<int, string> skillNameDictonary = new Dictionary<int, string> ();
foreach (Skill skill in skillList) {
    skillDictionary.Add (skill.Id, skill.Name);
}

 使い方は次のような感じでしょうか。

int skillId = GetSkillId ();  // 対象のId(int型)を取得
string skillName = skillDictonary [skillId];

 このようなDictionaryもLINQを使えば、1行で作れてしまいます。

ToDictionaryメソッドを使って、ListからDictionaryを生成
Dictionary<int, string> skillDictonary = skillList.ToDictionary (skill => skill.Id, skill => skill.Name);

 ここで使ったオーバーロードは、引数に二つのデリゲートをとります。イメージとしてはIEnumerable<T>のぞれぞれ各要素に対して、1つ目のデリゲートを適用した結果をキーとし、2つ目のデリゲートを適用した結果をバリューとしたキーとバリューのペアを作り、それらからDictionaryが作られるイメージです。

MSDNの構文は次の通りです。

ToDictionaryの構文2
public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    Func<TSource, TElement> elementSelector
)

えっ?Dictionary<TKey, List<TValue>>が欲しいって?

 前の二つのDictionaryはどちらも、int型のIdをキーとしていました。Idは重複がないものでしたので、Idに対応するSkillが一意に決まりましたね。

 もしElement型のキーからSkillを引きたい場合どうすればいいでしょうか?あるElementに対応するSkillは複数あり、Elementから一意のSkillに決定はできないのでDictionary<Element, Skill>は使えませんね。そこで次のような、Dictionary<Element, List<Skill>>型を作ることが考えられますね。

Dictionary<Element, List<Skill>> elementSkillListDictonary = new Dictionary<Element, List<Skill>>();
foreach (Skill skill in skillList) {
    if (elementSkillListDictonary.ContainsKey (skill.Element)) {
        elementSkillListDictonary[skill.Element].Add (skill);
    } else {
        elementSkillListDictonary[skill.Element] = new List<Skill> {skill}; 
    }
}

 長いですね...次のようにElementがFireのものを指定して引いたり、全要素をキーバリューペアごとにまわし、Elementのグループごとに処理をしたりもする使い方が考えられます。

List<Skill> fireSkillNameList = elementSkillListDictonary[Element.Fire];
foreach (Skill fireSkill in fireSkillNameList) {
    Debug.Log (fireSkill.Name);
}
foreach (KeyValuePair<Element, List<Skill>> item in elementSkillListDictonary) {
    Debug.Log (item.Key);
    List<Skill> skills = item.Value;
    foreach (Skill skill in skills) {
        Debug.Log (skill.Name);
    }
}

 使いどころはありそうですが、いかんせんバリューがListのDictionaryを作成するコードが長いですね。LINQで簡潔にかけいないのでしょうか。実はこの処理もLINQを使って1行で...残念ながらこれとまったく同じことをするLINQメソッドはありません。(まぁ複数のLINQメソッドを組み合わせることで、できなくはないのですが、それよりもいい方法があります。)

 上記とまったっく同じものを作るのではないのですが、LINQのToLookupというメソッドを使えばやりたいことを実現できると思います。

ToLookupメソッド、使ってみて!

 それでは、ToLookupメソッドを説明します。下記のコードはToLookupメソッドのサンプルです。

ToLookupの例
ILookup<Element, Skill> elementSkillLookup = skillList.ToLookup (skill => skill.Element);

 これだけ見ても何ができるか分かりませんね。ToLookupメソッドの返り値型、ILookupインターフェースのインスタンスelementSkillLookupで、さきほどDictionaryでやったことと同じことをやってみましょう。

IEnumerable<Skill> fireSkills = elementSkillLookup[Element.Fire];
foreach (Skill fireSkill in fireSkills) {
    Debug.Log (fireSkill.Name);
}

 List<Skill>型ではなく、IEnumerable<Skill>型ですが、Dictionaryのようにインデクサーが使えます。

foreach (IGrouping<Element, Skill> group in elementSkillLookup) {
    Debug.Log (group.Key);
    IEnumerable<Skill> skills = group;
    foreach (Skill skill in skills) { // groupでforeachすることもできる
        Debug.Log (skill.Name);
    }
}

 ILookup<TKey, TLement>インターフェースは、IEnumerable<IGrouping<TKey, TElement>>を継承しています。次のようにIGrouping<Element, Skill>ごとにforeachでまわすことができます。先のDictionaryのようにグループごとの処理も可能です。

 バリューがListのDictionaryでやったことが、ILookupでもできますね。

 さて、ToLookupメソッドもいくつかのオーバーロードがあります。ToDictionaryと同じように二つのデリゲートを引数にとるものがあります。

ILookup<Element, string> elementSkillNameLookup = skillList.ToLookup (
    skill => skill.Element,
    skill => skill.Name
);

IEnumerable<string> fireSkillNames = elementSkillNameLookup[Element.Fire];
foreach (string fireSkillName in fireSkillNames) {
    Debug.Log (fireSkillName);
}

foreach (IGrouping<Element, string> group in elementSkillNameLookup) {
    Debug.Log (group.Key);
    IEnumerable<string> skillNames = group;
    foreach (string skillName in skillNames) {
        Debug.Log (skillName);
    }
}

ちなみにToLookupとToDictionaryを用いて、バリューにListをもつDictionaryをつくることも可能です。

Dictionary<Element, List<Skill>> elementSkillListDictonary = skillList
    .ToLookup (skill => skill.Element)
    .ToDictionary (
        skillGroup => skillGroup.Key,
        skillGroup => skillGroup.ToList ()
    );

まとめ

 WhereやSelectなどIEnumerable<T>からIEnumerable<T>を作るメソッド、Any、All、Count、Sumなど真理値や数字を作るメソッドは、LINQを紹介する投稿やブログ、サンプルコードでも良く見ますね。

 それらに埋もれがちですが、List<T>や配列などからDictionaryを作るメソッド、ToDictionaryもあります。利用シーンは前述のメソッドに比べると多くないかもしれません。ですがそれを使えば簡潔に目的のDictionaryを生成できます。

 また、冗長になりがちなDictionary<TKey, List<TValue>>を生成するコード。ToLookupメソッドを用いて、ILookupを生成し、それで代用することが可能です。あまりブログなどでこのクラスは見かけませんが、ぜひ使ってみてください!