LoginSignup
252

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-08-16

はじめに

 みなさん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を生成し、それで代用することが可能です。あまりブログなどでこのクラスは見かけませんが、ぜひ使ってみてください!

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
252