CodeGym /コース /C# SELF /SelectManyでコレクションをフラ...

SelectManyでコレクションをフラットにする方法

C# SELF
レベル 31 , レッスン 4
使用可能

1. はじめに

「フラット」なオブジェクトのリスト(例えば、前のレクチャーでやった商品やユーザーのリスト)だけを扱うなら、めっちゃシンプルだよね:フィルタして、変換して、ソートするだけ。でも実際のビジネスアプリや面接(そして宿題でも!)では、コレクションの中にさらにコレクションが入ってることがよくあるんだ。

例えば、Userクラスがあって、その中に注文リストがあるとか。あるいはProductクラスにレビューのリストがあるとか。で、課題:全ユーザーの全注文のリストを取得したい?それとも全商品の全レビューを集めたい?それとも全注文の中の全商品をリストアップしたい?

こういう時、普通のSelectやC#の魔法だけじゃ足りないことがある。そこで登場するのがヒーロー、SelectManyオペレーター!「リストのリスト」をただの「リスト」に変えてくれるんだ。

キッチンのたとえ話

もし君の手元に箱があって、その中にまた箱があって、その中にクッキーが入ってるとする。全部のクッキーを一つの大きなボウルに出したい時、いちいち箱ごとに一個ずつクッキーを出すんじゃなくて、全部の箱から一気にクッキーをボウルに入れるよね。

これがSelectManyのやってること。「箱の中の箱」を一つの「ボウル」に中身ごとぶちまける感じ!

2. アプリの例を思い出そう

前のレクチャーでは、ユーザーや商品を扱うシンプルなアプリを作ったよね。今回は「ネストされた」要素のコレクションを追加してみるよ。

例えば、こんなクラスがあるとしよう:


public class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public List<Product> Products { get; set; }
}

public class Product
{
    public string Name { get; set; }
}

今、ユーザーのリストがあって、それぞれのユーザーに注文リストがあって、各注文には商品リストがあるって感じ。

全ユーザーの全注文の全商品のリストをどうやって取得する?

全部の商品を一つの「ボウル」にまとめたいとしよう。普通のSelectを使ったらどうなる?


List<User> users = ...; // どこかでデータがあると仮定

var allOrders = users.Select(u => u.Orders);

allOrdersの型は... IEnumerable<List<Order>>になる。つまり「リストのリスト」、注文コレクションの集まりだね。本当は一つのフラットな注文リストが欲しいのに。

もう一回やってみると:


var allProducts = users.Select(u => u.Orders)
                       .Select(oList => oList.Select(o => o.Products));

今度はどう?今度は「リストのリストのリスト」になっちゃう!どんどん構造が複雑になって、全然シンプルにならないよね。

3. 解決策:SelectManyオペレーター

ここでSelectManyの本領発揮。こいつの仕事は、ネストされたコレクションを「フラット化(flatten)」すること。

基本の書き方はこんな感じ:


collection.SelectMany(item => item.中のコレクション)

今回の場合は:


var allOrders = users.SelectMany(u => u.Orders);
// 型: IEnumerable<Order>

これで全ユーザーの全注文がただのシーケンスになる。

さらに深掘り!全ユーザーの全注文の全商品をゲット:


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);
// 型: IEnumerable<Product>

一行で書くなら?

もちろん一行でもいける:


var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));

このチェーンは、特にロボットじゃないなら、分けて読んだ方がいいかも。

4. でっかい例で分解してみよう

テストデータを作って、SelectManyがどう動くか見てみよう。


var users = new List<User>
{
    new User
    {
        Name = "アリサ",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 1,
                Products = new List<Product>
                {
                    new Product { Name = "クッキー" },
                    new Product { Name = "ミルク" },
                }
            },
            new Order
            {
                Id = 2,
                Products = new List<Product>
                {
                    new Product { Name = "チョコレート" }
                }
            }
        }
    },
    new User
    {
        Name = "ボブ",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 3,
                Products = new List<Product>
                {
                    new Product { Name = "コーヒー" },
                    new Product { Name = "お茶" }
                }
            }
        }
    }
};

全部の商品を一つのボウルに「ぶちまける」


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);

foreach (var product in allProducts)
{
    Console.WriteLine(product.Name);
}

プログラムの出力結果:

クッキー
ミルク
チョコレート
コーヒー
お茶

各ステップで何が起きてる?

  1. users.SelectMany(u => u.Orders) — 全ユーザーの注文を「展開」して一つの注文コレクションに(今は3つの注文)。
  2. .SelectMany(o => o.Products) — 全注文から商品リストを展開。

注意: 普通のSelectを使うと、コレクションのコレクション(例えばIEnumerable<List<Product>>)になる。SelectManyなら「フラット」な商品コレクション(IEnumerable<Product>)になるよ。

5. 図で見るとこうなる

SelectMany


users (List<User>)
   └── User #1
   │      └── Orders (List<Order>)
   │           ├── Order #1 -> Products (List<Product>)
   │           └── Order #2 -> Products (List<Product>)
   └── User #2
          └── Orders (List<Order>)
               └── Order #3 -> Products (List<Product>)

最初のSelectMany(u => u.Orders)


IEnumerable<Order>: [Order #1, Order #2, Order #3]

2回目のSelectMany(o => o.Products)


IEnumerable<Product>: [クッキー, ミルク, チョコレート, コーヒー, お茶]

6. 比較:SelectSelectMany

Select SelectMany
返すもの 「コレクションのコレクション」 フラットなコレクション
users.Select(u => u.Orders)
IEnumerable<List<Order>>
users.SelectMany(u => u.Orders)
IEnumerable<Order>
使いどころ ネスト構造を残したい時 一つのフラットなシーケンスが欲しい時

表:どっちのオペレーターを使う?

やりたいこと 使うもの 結果
リストのリストを取得(フラット化しない) Select
IEnumerable<ICollection<T>>
一つの「フラット」なシーケンスを取得 SelectMany
IEnumerable<T>
「親-子」ペアを取得 SelectMany+結果セレクター
IEnumerable<CustomType>
「フラット」なコレクションの要素だけ変換 Select
IEnumerable<TransformedType>

7. 便利な小ネタ

クエリ構文(「SQLっぽい」書き方が好きな人向け)

表現力重視派のために — LINQはSQLっぽいクエリ構文でも書けるよ:


var allProducts = from user in users
                  from order in user.Orders
                  from product in order.Products
                  select product;

このコードはSelectManyのチェーンと同じことをしてるけど、見た目は多段階の「from」って感じ。

「行列(マトリックス)」のフラット化

SelectManyの定番の使い方は、2次元配列やリストのリストを扱う時。


List<List<int>> matrix = new List<List<int>>
{
    new List<int> { 1, 2, 3 },
    new List<int> { 4, 5 },
    new List<int> { 6 }
};

全部の数字を一つのリストにしたい時は?


var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
    Console.Write(value + " "); // 1 2 3 4 5 6
}

8. ちょっと進んだ書き方

SelectManyのオーバーロードを使うと、内側の要素だけじゃなくて外側の情報も返せるんだ。

例えば:各注文の各商品の「ユーザー名—商品名」ペアを取得したい場合。


var userProductPairs = users.SelectMany(
    user => user.Orders.SelectMany(
        order => order.Products,
        (order, product) => new { UserName = user.Name, ProductName = product.Name }
    )
);

foreach (var pair in userProductPairs)
{
    Console.WriteLine($"{pair.UserName}が{pair.ProductName}を注文した");
}

ここでラムダの2番目の引数は外側のコレクションの要素。外側の情報を「失わない」ために超便利!

9. 実践シナリオ

たくさんの注文、たくさんの商品

君はネットショップの開発者。全顧客が買ったユニークな商品数を数えたい?こうやる:


var uniqueProductNames = users
    .SelectMany(user => user.Orders)
    .SelectMany(order => order.Products)
    .Select(product => product.Name)
    .Distinct();

foreach (var name in uniqueProductNames)
{
    Console.WriteLine(name);
}

階層からフラットなコレクションを作る

会社の部署リストがあって、各部署に社員がいる。全社員のリストを作りたい時:


var allEmployees = departments.SelectMany(d => d.Employees);

文字列操作:「単語配列」を文字配列にフラット化


string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
    Console.Write(c + " "); // h e l l o w o r l d
}

10. よくあるミスと注意点

SelectManyのパワーに感動したところで、よくあるミスもチェックしとこう。

一番ありがちなのは、Selectで「フラット」なコレクションが返ると思い込むこと。実際は、内側の関数がコレクションを返すと、必ずコレクションのコレクションになる。結局、二重ループを書いたり、型で混乱したりしがち。

二つ目のミスは、「フラット化」で外側の情報を失うことに気づかないこと。例えば、商品だけ扱ってると、どの注文やどのユーザーのものか分からなくなる。結果セレクター付きのオーバーロードを使わないとね。

三つ目の注意点は、内側のリストがnullかもしれない場合。SelectManyを使う前に、フィルタするか空リストに置き換えないと、NullReferenceExceptionがF5を押すより早く出るから気をつけて!

2
タスク
C# SELF, レベル 31, レッスン 4
ロック未解除
ネストされたリストからすべてのアイテムを抽出する
ネストされたリストからすべてのアイテムを抽出する
1
アンケート/クイズ
LINQへのイントロ、レベル 31、レッスン 4
使用不可
LINQへのイントロ
LINQの基本:シンプルなクエリとデータのフィルタリング
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION