はじめに
みなさんLINQ使っていますか?LINQ最高ですよね!
さて、ListからDictionary作るようなことをしませんか?空のDictionaryを作って、foreach文を使ってListをまわし、Dictionaryに要素を追加していってDictionaryを作るコードなどを書きませんか?
実はLINQを使って非常に簡潔に、ListなどのクラスからDictionaryを作ることができるのです。
Dictionaryを作る時、もしかしたらこんなコード書きません?
こんな列挙型とクラスがあります。
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>を作ります。次のようなコードです。
Dictionary<int, Skill> skillDictonary = new Dictionary<int, Skill> ();
foreach (Skill skill in skillList) {
skillDictionary.Add (skill.Id, skill);
}
使うとしたらこんなかんじでしょうか。
int skillId = GetTargetSkillId (); // 対象のId(int型)を取得
Skill targetSkill = skillDictionary [skillId];
どうでしょう、このようなListからDictionaryを作るコードを書いたことはありませんか?実は、このようなコードはLINQを使えば1行で書けてしまうのです。
LINQのToDictionaryメソッドを使って1発でDictionaryを作成!
LINQのToDictionaryメソッドを使って、さきほどのコードを書き換えます。
Dictionary<int, Skill> skillDictonary = skillList.ToDictionary (skill => skill.Id);
非常に簡潔ですね。1行で書けてしまいました!これだけのコード量で、目的のDictionaryができてしまいました。
ToDictionaryメソッドはLINQのメソッドで、List<T>型でなくても、IEnumeralble<T>型を実装したクラスのインスタンスであれば利用できます。ToDictionaryメソッドは他のLINQのメソッドと同様に、複数のオーバーロードを持っています。
ここで使ったオーバーロードは、引数にデリゲートをとります。IEnumerable<T>の各要素がDictionaryのバリューになります。引数に渡したデリゲートを、各要素(バリュー)に適用した結果が要素(バリュー)に対応するキーとなります。
MSDNに記載されている構文を次に示します。
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>を作ることもあるのではないでしょうか?
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行で作れてしまいます。
Dictionary<int, string> skillDictonary = skillList.ToDictionary (skill => skill.Id, skill => skill.Name);
ここで使ったオーバーロードは、引数に二つのデリゲートをとります。イメージとしてはIEnumerable<T>のぞれぞれ各要素に対して、1つ目のデリゲートを適用した結果をキーとし、2つ目のデリゲートを適用した結果をバリューとしたキーとバリューのペアを作り、それらからDictionaryが作られるイメージです。
MSDNの構文は次の通りです。
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メソッドのサンプルです。
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を生成し、それで代用することが可能です。あまりブログなどでこのクラスは見かけませんが、ぜひ使ってみてください!