CodeGym /コース /C# SELF /複数のソースをまとめる: SelectMany

複数のソースをまとめる: SelectMany のLINQでの使い方

C# SELF
レベル 33, レッスン 2
使用可能

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
SelectMany 用のネストコレクション図

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 の違いの図

ざっくり言うと:

  • Select → 「ユーザーごとの注文リストをちょうだい」
  • SelectMany → 「全部の注文をまとめてちょうだい」
2
タスク
C# SELF, レベル 33, レッスン 2
ロック未解除
SelectManyを使ってコレクションを結合しよう
SelectManyを使ってコレクションを結合しよう
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION