73
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[C#・LINQ]九九だけじゃない!アプリ開発にもゲーム開発にも使える、SelectMany!

Last updated at Posted at 2014-04-20

はじめに

LINQ To ObjectsのSelectMany。

恥ずかしながら私は、LINQのSelectやWhere、Firstを使うようになってから、このメソッドの使い方を理解するまで時間が少し空いてしまいました。その後、SelectManyの使い方を知ったとき、「あのfor文いらなかった」、「あれ、もっとスッキリ書けた」と後悔してしまいました。

「SelectMany?なにそれ?」や「SelectMany?あー名前は知っているけど、使っていないやー」という方は最後まで読まなくても、最初の方だけ読んでいただいて、SelectManyをとりあえず試しに使ってもらえると嬉しいです。

また、SelectManyはたまに九九の表作成の例が紹介されています。SelectManyは九九に使うものと思っている方が、もっとたくさんの場面で使えるという考えを持っていただけると嬉しいです。

SelectManyはXamarinを用いたアプリ開発や、Unityでのゲーム開発に置いて、活躍する場面も多いと思っています。

SelectManyの使い方は?

自分が思いつくSelectManyの使い方を3例紹介します。(もちろん他にもあると思います。)

リストのリストの平坦化

こんなコードを書いた方は多いのではないでしょうか?

foreach文でリストの平坦化
List<List<string>> listOfNameList = LoadListOfNameList();
List<string> nameList = new List<string> ();
foreach(List<string> names in listOfNameList) {
	nameList.AddRange(names);
}

List<List<string>>をList<string>にしていますね。
このような処理はSelectManyでスッキリと書き変えることができます。

SelectManyでリストの平坦化
List<List<string>> listOfNameList = LoadListOfNameList();
List<string> nameList = listOfNameList.SelectMany( names => names ).ToList();

すっきりしましたね!
この処理は、他の言語だとflatten(引数なし)というメソッドで表されることもありますね。

要素XのリストをもつオブジェクトY。YのリストからXのリストを作る

次のPersonクラスのリスト(List<Person>)から、全員の趣味のリスト(List<string>)を作りたいとします。

Personクラス
public class Person
{
	public string Name { get; set; }
	public List<string> Hobbies { get; set; }
}

まずforeach文で書いてみます。

foreach文でリストの平坦化
List<Person> persons = LoadAllPerson();
List<string> hobbyList = new List<string> ();
foreach(Person person in persons) {
	hobbyList.AddRange(person.Hobbies);
}

これをSelectManyで書き換えます。

SelectManyのリストの平坦化
List<Person> persons = LoadAllPerson();
List<string> hobbyList = persons.SelectMany( person => person.Hobbies ).ToList();

foreach文が無くなって、スッキリしましたね!!!

要素XのリストをもつオブジェクトY。Yのリストから、個々のXとそれに対応するYで新たなオブジェクトZを作り、Zのリストを作る

まず、例で使うコードを見て下さい。

Skillクラス
public class Skill {
public String Name{ get; set; }
	// 必要最小限。MPとか
}

説明文のXに対応するSkillクラスです。

Characterクラス
public class Character {
	public string Name { get; set; }
	public List<Skill> Skills { get; set; }
	// 必要最小限。MPとかHPとか本当だったらもっとある。
}

説明文ではYに対応するCharacterクラスです。Skillのリスト(要素Xのリスト)と名前を表すNameを持っています。

SkillViewクラス
public class SkillView {
	public string OwnerName { get; set; }
	public string SkillName { get; set; }
	// 必要最小限。実際はもっといろいろある。
}

このSkillViewは説明文中のZに対応します。

X、Y、Zに型をあてはめて、やりたいことを説明します。
「要素SkillのリストをもつオブジェクトCharacter。Characterのリストから、個々のSkillとそれに対応するCharacterで新たなオブジェクトSkillViewを作り、SkillViewのリストを作る」

Xamarinを用いたアプリ開発やUnityでのゲーム開発で、このような処理をするのではないでしょうか?

これもまずforeach文で書いてみます。

foreach文でCharacterのListからSkillViewのListを作成(再掲)
List<Character> characters = LoadAllCharacters ();

List<SkillView> skillViews = new List<SkillView> ();
foreach (Character character in characters) {
	foreach (Skill skill in character.Skills) {
		SkillView skillView = new SkillView {
			OwnerName = character.Name,
			SkillName =  skill.Name
		};
		skillViews.Add (skillView);
	}
}

二重のforeach文を用いて、List<Character>のcharactersをList<SkillView>のskillViewsに変換することができました。長いですね。

これをLINQで書き換えてみます。

LINQでCharacterのListからSkillViewのListを作成(再掲)
List<SkillView> skillViews = characters.SelectMany (
		(character => character.Skills), 
		(skillOwner, skill) => new SkillView{ OwnerName =  skillOwner.Name, SkillName = skill.Name})
.ToList();

すっきりしましたね。
すっきりしましたが、初見だと「これはいったいなんだ?、何をしているんだ?」と思った方もいたのではないでしょうか?

ここまでのまとめ

さて、

  • リストのリストを平坦化してリストにする
  • 要素XのリストをもつオブジェクトY。YのリストからXのリストを作る
  • 要素XのリストをもつオブジェクトY。Yのリストから、個々のXとそれに対応するYで新たなオブジェクトZを作り、Zのリストを作る

 3つの例を駆け足で紹介させていただきました。SelectManyを「あ、これはあの場面で使えた!」と思われた方もいるのでは無いでしょうか?

 ただここまでで、SelectManyの引数に渡したラムダ式の意味や処理について何も触れませんでした。実は同じSelectManyメソッドでも、実は1、2個目の例と3個目の例で違うオーバーロードを使っています。1、2個目の例は引数のデリゲートが1個、3個目の例は2個でしたね。また、1個目の例は SelectManyの引数に names => names というラムダ式をデリゲートとして渡してます。渡されたものをそのまま渡すラムダ式、これはいったいどういう意味があるのでしょうか?3個目の例は、一体何を表しているのか?

 ここからは少し突っ込んだことを書いていこうと思います。大分長くなってしまいました。SelectManyは便利!ここで満足、という方ありがとうございました。

それぞれの例を突っ込んで考える

 順番を変えて1個目の例の「リストのリストを平坦化してリストにする」の前にまず、「要素XのリストをもつオブジェクトY。YのリストからXのリストを作る」を見てみます。

「要素XのリストをもつオブジェクトY。YのリストからXのリストを作る」を突っ込んで考える

「要素XのリストをもつオブジェクトY。YのリストからXのリストを作る」に関連するコードを再掲します。

Personクラス
public class Person
{
	public string Name { get; set; }
	public List<string> Hobbies { get; set; }
}
SelectManyのリストの平坦化
List<Person> persons = LoadAllPerson();
List<string> hobbyList = persons.SelectMany( person => person.Hobbies ).ToList();

この例で使っているSelectManyメソッドは実際は、Enumerableクラスのクラスメソッド、「Enumerable.SelectMany<TSource, TResult> 」です。これは、IEnumerable<T>型の拡張メソッドになっています。MSDNのリファレンスはこちらです。こちらから説明と構文を持ってきました。

説明
シーケンスの各要素を IEnumerable に射影し、結果のシーケンスを 1 つのシーケンスに平坦化します。

構文

構文
public static IEnumerable<TResult> SelectMany<TSource, TResult>(
	this IEnumerable<TSource> source,
	Func<TSource, IEnumerable<TResult>> selector
)

さて、構文をみていきましょう。

まず第一引数、thisがついていますね。他のLINQと同じように、IEnumerable<TSource>の拡張メソッドとして、Enumerableクラス内に定義されているということですね。

次に第二引数のselctor、Func<TSource, IEnumerable<TResult>>型のデリゲート。「TSource型をもらって、IEnumerable<TResult>型を返す(射影する)デリゲート」を渡せばよいですね。

最後に返り値は、IEnumerable<TResult>型ですね。説明から読み取るに、selectorで返した結果(IEnumerable<TResult>)を連結し、1つのシーケンス(IEnumerable<TResult>)として出力する(平坦化する)のですね。

前述の例とこの構文を突きあわせて考えてみます。TSourceやTResltが何か、引数に渡すデリゲートが実際はどのような処理なのでしょうか?

personsはList<Person>ですから、TSourceはPersonですね。
結果が、IEnumerable<string>型ですので(ToListでList<string>にしています)、TResultはstring型ですね。

ということは、

Func<TSource, IEnumerable<TResult>>

のselectorはここでは、

Func<Person, IEnumerable<string>>

ですね。

SelectManyの引数のラムダ式をわざと冗長に書きかえて、変数に代入してみます。

SelectManyのリストの平坦化をわざと冗長に
List<List> persons = LoadAllPerson();

Func<Person, IEnumerable<string>> selector = (Person person) => { 
	IEnumerable<string> hobbies = person.Hobbies;
	return hobbies;
};

List<string> hobbyList = persons.SelectMany(selector).ToList();

※ここでのリストは正確にはIEnumerable<T>です

SelectManyの引数に渡すデリゲートは、

  • 引数はSelectManyを呼び出すリストの要素の型(この例ではPerson)
  • 返り値は生成したいリスト型と同じリスト型(この例ではIEnumerable<string>)

とすればいいですね。

##「リストのリストの平坦化」を突っ込んで見てみる

順番は前後しましたが、「リストのリストの平坦化」という例とSelectManyの構文から、それぞれの型が何なのか考えてみます。こちらの方が前の例より若干複雑です。
こちらも前の例と同じオーバーロードを使っています。MSDNのリファレンスはこちら

構文(再掲)
public static IEnumerable<TResult> SelectMany<TSource, TResult>(
	this IEnumerable<TSource> source,
	Func<TSource, IEnumerable<TResult>> selector
)
SelectManyでリストの平坦化(再掲)
List<List<string>> listOfNameList = LoadListOfNameList();
List<string> nameList = listOfNameList.SelectMany( names => names ).ToList();

names => names というラムダ式は一見シンプルですね。

TSourceや、TResultはここでの型はなんでしょうか?

listOfNameListはList<List<string>>ですから、TSourceはList<string>ですね。ここでの注意は、TSouceはstringでなくて、List<string>ということです。
結果が、IEnumerable<string>型ですので(ToListでList<string>にしています)、TResultはstring型ですね。

ということは、

Func<TSource, IEnumerable<TResult>>

なselectorは、ここでは

Func<List<string>, IEnumerable<string>>

ですね。

SelectManyの引数のラムダ式をわざと冗長に書きかえて、変数に代入してみます。

SelectManyのリストの平坦化をわざと冗長に
List<List<string>> listOfNameList = LoadListOfNameList();

Func<List<string>, IEnumerable<string>> selector = (List<string> list) => {
	IEnumerable<string> names = list;
	return names;
};

List<string> nameList = listOfNameList.SelectMany(selector).ToList();

繰り返しになりますが、TSourceがList<string>で、TResultがstringです。
SelectManyに渡すデリゲートは、TSouceを渡して、IEnumerable<TResult>を変えします。この例では、List<string>を渡して、IEnumerable<string>を返しています。names => names という表記は一見そのまま渡しているだけに見えますが、型を踏まえて考えるとなかなか奥が深いですね。

「要素XのリストをもつオブジェクトY。Yのリストから、個々のXとそれに対応するYで新たなオブジェクトZを作り、Zのリストを作る」を突っ込んで見てみる

前の2例とこの例はSelectManyの異なるオーバーロードを使っています。

おさらい

Skillクラス(再掲)
public class Skill {
	public string Name{ get; set; }
	// 必要最小限。MPとか
}
Characterクラス(再掲)
public class Character {
	public string Name { get; set; }
	public List<Skill> Skills { get; set; }
	// 必要最小限。MPとかHPとか本当だったらもっとある。
}
SkillViewクラス(再掲)
public class SkillView {
	public string OwnerName { get; set; }
	public string SkillName { get; set; }
	// 必要最小限。実際はもっといろいろある。
}
LINQでCharacterのListからSkillViewのListを作成(再掲)
List<SkillView> skillViews = characters.SelectMany (
		(character => character.Skills), 
		(skillOwner, skill) => new SkillView{ OwnerName =  skillOwner.Name, SkillName = skill.Name})
.ToList();

まず構文をみてみる

この例で使っているSelectManyメソッドは前の2例と違うオーバーロードです。引数のデリゲートの数が違いますね。

この例のメソッドは、Enumerableクラスのクラスメソッド、「SelectMany<TSource, TCollection, TResult>」です。これも拡張メソッドになっています。MSDNのリファレンスはこちらです。こちらから説明と構文を持ってきました。

説明
シーケンスの各要素を IEnumerable に射影し、結果のシーケンスを 1 つのシーケンスに平坦化して、その各要素に対して結果のセレクター関数を呼び出します。

構文

構文
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
	this IEnumerable<TSource> source,
	Func<TSource, IEnumerable<TCollection>> collectionSelector,
	Func<TSource, TCollection, TResult> resultSelector
)

SelectMany<TSource, TCollection, TResult>とあります。前のオーバーロードと比べて、型引数TCollectionが増えていますね。説明文から前のオーバーロードとシーケンスを平坦化するのは同じですね。また、平坦化した中間要素の各要素に対して引数で渡したデリゲートで射影を行うようですね。

冗長に書いたコードとこの例での型を見てみる

構文を見る前に冗長に書いたコードを見てみます。

a
// Func<TSource, IEnumerable<TCollection>>
Func<Character, IEnumerable<Skill>> collectionSelector = (Character character) => { 
	IEnumerable<Skill> skills = character.Skills;
	return skills;
};

// Func<TSource, TCollection, TResult>
Func<Character, Skill, SkillView> resultSelector = (Character skillOwner, Skill skill) => {
	SkillView skillView = new SkillView{
		OwnerName = skillOwner.Name,
		SkillName = skill.Name
	};
	return skillView;
};

List<SkillView> skillViews = characters.SelectMany (collectionSelector, resultSelector).ToList ();

それでは構文と照らし合わせて型やデリゲートを見ていきます。

構文(再掲)
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
	this IEnumerable<TSource> source,
	Func<TSource, IEnumerable<TCollection>> collectionSelector,
	Func<TSource, TCollection, TResult> resultSelector
)

まずSelectManyの返り値、この例ではIEnumerable<SkillView>ですね。(そのあとToListでList<SkillView>にしています。)TResult型はSkillViewですね。

第一引数から、IEnumerable<TSource>の拡張メソッドということが分かります。そしてTSouce型はCharacter型ですね。

次に第二引数collectionSelectorは、Func<TSource, IEnumerable<TCollection>>型のデリゲートですね。これは前のオーバーロードのselectorに似ていますね。前のオーバーロードから型パラメータが、TResultからTCollectionに変わっています。この例では、Character型を引数に取り、IEnumerable<Skill>を返すデリゲートになっています。TCollectionはSkill型ですね。このデリゲートで一回平坦化を中間要素(下ごしらえ、射影する前の状態)を作るようですね。

そして第三引数のresultSelector。Func<TSource, TCollection, TResult>型ですね。TSource型とTCollection型を貰ってTResultを返す。ここでは、CharacterとSkillを貰って、SkillViewを返すデリゲートですね。第二引数のcollectionSelectorで1回平坦化した中間結果に対して、このデリゲートで射影をするようですね。

ざっくり言うと

ざっくり言うと、

  • 第ニ引数のデリゲートで平坦化する(※ただし次に射影する前の下ごしらえ状態)
  • 第三引数のデリゲートで平坦化した要素を射影する(※ただし平坦化した要素の所属元といっしょに)

で、最終的に一つのIEnumerable<TResult>になる。とでも言うのでしょうか?

SelectMany(引数のデリゲート二つ)をSelectMany(引数のデリゲート一つ)で書き換える

引数のデリゲートが一つのオーバーロードを使って、この例を書けないかやってみました。

できました。

まず、元の方のSelectMany(引数のデリゲート二つ)

LINQでCharacterのListからSkillViewのListを作成(再掲)
List<SkillView> skillViews = characters.SelectMany (
		(character => character.Skills), 
		(skillOwner, skill) => new SkillView{ OwnerName =  skillOwner.Name, SkillName = skill.Name})
.ToList();

次に書き換えたもの。

SelectMany(デリゲート一つ)とSelectで再現
List<SkillView> skillViews = characters
	.SelectMany (character => character.Skills.Select (
		skill => new SkillView { OwnerName = character.Name, SkillName = skill.Name}
	))
	.ToList();

うーん。うーん。
僕は引数にデリゲートを二つ取る方の書き方が好きです。

この例のミソは、最終成果物のSkillViewの生成にCharacterとSkill両方が必要ということだと思います。デリゲート二つのSelectManyのオーバーロードは、それぞれのデリゲートを素直に記述することが可能です。ですが、デリゲートを1つ引数に取るオーバーロードでは、一つのデリゲートの中でこねくり回している印象を受けます。

このような場合は、引数にデリゲートを二つ取る方の書き方を採用するべきかと思います。

SelectManyのオーバーロードについて

ここまでで、SelectManyのオーバーロードを二つ紹介しました。

  • デリゲートを1個とるもの(selector)
  • デリゲートを2個とるもの(collectionSelectorとresultSelectr)

です。

実は、SelectManyのオーバーロードは四個あります。ですが大別すると二つと考えていいと思います。

今まで紹介した二つのオーバーロードそれぞれに、インデックス付きで処理が行えるのかどうかの違いがあるオーバーロードがあります。Selectメソッドのインデックス付きのオーバーロードと似ていますね。(これについて、以前投稿しました。【C#,LINQ】インデックス付きで射影(Select)と抽出(Where)【iが欲しい!?】他の言語もちょっと。)

2[個,引数のデリゲートの違い] × 2[個,インデックス付きの違い] = 4[個]

の計4個のオーバーロードですね。

まとめ

SelectMany便利ですね!

リストのリストを平坦化する際は、もうforeach文はいりませんね。

Xamarnでのアプリ開発やUnityでのゲーム開発で活用できる場面が多いのではないでしょうか?

73
71
0

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
73
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?