1. はじめに
普通の Select がどう動くか思い出してみて:
var names = users.Select(user => user.Name);
各ユーザーに対してその名前を取り出して、名前のリストができる。超シンプル!でも、全ユーザーの全注文リストが欲しい場合は?
var ordersList = users.Select(user => user.Orders);
これだとどうなる?各ユーザーごとに注文リストが返ってくる。つまり結果はコレクションのコレクション(IEnumerable<List<Order>>)になる。箱が棚に並んでるイメージで、注文1つ1つを見るには、まず箱(ユーザーの注文リスト)を1つずつ開けていかないといけない。
全部まとめて「アンパック」して、普通の注文リストとして扱いたいよね。そこで SelectMany の出番!
たとえ話:箱の中身を全部出す
例えば、全部の持ち物がバラバラじゃなくて箱に入ってるとする。各箱はコレクション、例えば1人のユーザーの買い物リスト。全部の持ち物を一気に見たい時、いちいち箱を開けて中身を確認するより、全部の箱の中身を一つの山にぶちまけた方が早いよね。これが SelectMany のイメージ!
2. SelectMany って何?
SelectMany はLINQのメソッドで、コレクションのコレクションを1つの「フラット」なコレクションに変換してくれる。「フラット」ってのは、結果のシーケンスにもう内部リストがなくて、全部の要素が1階層に並ぶってこと。マトリョーシカじゃなくて、ただの長い値の列になる感じ。
メソッドのシグネチャはこんな感じ:
IEnumerable<TResult> SelectMany<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TResult>> selector
)
使い方は簡単。コレクション(例えばユーザーリスト)があって、各要素がまた別のコレクション(例えばそのユーザーの注文リスト)を持ってる。SelectMany は各ユーザーから注文を取り出して、全部まとめて1つの注文ストリームにしてくれる。ラッピングもネストもなし!「リストのリスト」から普通のリストにしたい時に超便利。
3. シンプルな例
例1:ユーザーリストとその注文
アプリのモデルはこんな感じ(前のレッスンから使ってるやつね):
// 前のレッスンで使ったクラス
class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
}
ユーザーリストがあるとする:
var users = new List<User>
{
new User
{
Name = "イワン",
Orders = new List<Order>
{
new Order { Id = 1, ProductName = "本" },
new Order { Id = 2, ProductName = "ペン" }
}
},
new User
{
Name = "マリア",
Orders = new List<Order>
{
new Order { Id = 3, ProductName = "ノートパソコン" }
}
}
};
やりたいこと:全ユーザーが今まで買った全商品のリストを作りたい。
普通の Select:
var ordersPerUser = users.Select(user => user.Orders);
// IEnumerable<List<Order>>
これだと「箱」がたくさんできて、それぞれにユーザーごとの注文リストが入ってるだけ。
SelectMany:
var allOrders = users.SelectMany(user => user.Orders);
// IEnumerable<Order>
これで全注文が1つの「長い」コレクションになる!
イメージ図
| ユーザー | 注文 |
|---|---|
| イワン | 本, ペン |
| マリア | ノートパソコン |
- Selectの後: [[本, ペン], [ノートパソコン]] — リストのリスト
- SelectManyの後: [本, ペン, ノートパソコン] — 1つのリスト
例2:多重ネストの扱い
例えば、注文に商品リスト(カート)がある場合、SelectMany を何回も使える。マトリョーシカみたいな感じ!
class Product
{
public string Name { get; set; }
}
class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
var users = new List<User>
{
new User
{
Name = "ピョートル",
Orders = new List<Order>
{
new Order
{
Id = 10,
Products = new List<Product>
{
new Product { Name = "スマホ" },
new Product { Name = "充電器" }
}
}
}
}
};
全ユーザーの全注文の全商品を取り出すには、SelectMany を2回使う:
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// IEnumerable<Product>
こうやって、まず1段階「箱」を開けて、さらに次の段階も開けていく感じ。
例3:Query Syntaxでの SelectMany
SQLっぽいLINQの書き方(Query Syntax)が好きなら、SelectMany は from を何回も書くことで表現できるよ:
var allProducts =
from user in users
from order in user.Orders
from product in order.Products
select product;
ここでは、次の from が前のコレクションから要素を取り出してる。
users
└─ user1
└─ order1
└─ product1, product2
└─ order2
└─ product3
└─ user2
└─ order3
└─ product4
Query Syntax は全部のパスをたどって、1つの長い商品リストを返してくれる。
4. 使い方のバリエーションとコツ
SelectManyで変換やフィルタもできる
SelectMany では、ただアンパックするだけじゃなくて、同時にフィルタや変換もできる。例えば、全注文から値段が1000ユーロ超えの商品だけ選ぶとか:
var expensiveProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products)
.Where(p => p.Price > 1000);
または、SelectMany の中でロジックをちょっと入れてもOK(セレクタをネストする):
var expensiveProducts = users
.SelectMany(u => u.Orders.SelectMany(o => o.Products))
.Where(p => p.Price > 1000);
どっちも好みの問題。どちらも処理を順番に分けてるだけ。
プロジェクション付きSelectManyの使い方
ちょっと「上級」なオーバーロードを使うと、アンパックしながら同時に結果を作れる。例えば「ユーザー名—商品名」のペアを作るとか:
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(order => order.Products),
(user, product) => new { user.Name, product.Name }
);
どう動くかというと、最初の引数 user => user.Orders.SelectMany(order => order.Products) で全商品のアンパック、2つ目の引数でユーザーと商品を組み合わせて新しいオブジェクトを作ってる。
5. 注意点とよくあるミス
SelectMany を付け忘れて、フラットなシーケンスじゃなくて「箱の配列」になっちゃうことがある。例えば、普通の Select を使っちゃって、全部の要素を直接処理できなくなるパターン。
コレクションがネストしてると、特に深い階層だと混乱しやすい。ルールとして、「リストのリスト」じゃなくてフラットなリストが欲しい時は、迷わず SelectMany を使おう!
どんな時に役立つ?
「オブジェクトコレクションがあって、その各要素がまたコレクションを持ってる」ってケース(例えば「ユーザー—注文」「コース—学生」「ニュース—コメント」など)なら、SelectMany の出番。面接でもよく聞かれるテーマで、Select と SelectMany の違いをちゃんと説明できて、コレクションを正しくアンパックできるかがポイント。
実際のプロジェクトでも、余計なネストなしでスッキリ速いコードが書ける。 .NETプラットフォームでは「多層コレクション」処理の標準で最適な方法だよ。
Select と SelectMany の違い
Select は構造をそのまま残す:ユーザーごとに注文リストがあるコレクションに使うと、結果はコレクションのコレクション、例えば [[A, B], [C]] になる。グループごと(例えばユーザーごと)にデータを扱いたい時はこれでOK。
でも SelectMany は「フラット化」して、全部のネストされたコレクションを1つのシーケンスにまとめる:[A, B, C]。グループ分けがいらなくて、全部の中身を一気に処理したい時(例えば全商品の検索とか)に便利。
+---------------------+ +--------------------+
| users | | allOrders |
+---------------------+ +--------------------+
| イワン: [A, B] | --+ +-> Order A |
| マリア: [C] | --+ | +-> Order B |
+---------------------+ | | +-> Order C |
| | +--------------------+
Select: | |
[[A, B], [C]] <--------+ +
|
SelectMany: <--------+
[A, B, C]
ざっくり言うと:
- Select → 「ユーザーごとの注文リストをちょうだい」
- SelectMany → 「全部の注文をまとめてちょうだい」
GO TO FULL VERSION