【C#】LINQのSelectManyで階層構造のリストを平坦化(フラット)にする

オブジェクトが別のコレクションをプロパティとして持っている場合、それらをまとめて一つのリストとして扱いたい場面があります。これを「平坦化(Flatten)」と呼びます。

通常のSelectメソッドでは「リストのリスト(入れ子構造)」になってしまい扱いづらいデータも、SelectManyメソッドを使用することで、階層を一段階引き上げ、単一のシーケンスとして扱うことが可能になります。

今回は、音楽配信サービスにおける「ユーザーが作成したプレイリスト」から、そこに含まれる「全てのジャンル」を洗い出すシナリオを題材に、SelectManyの挙動と活用法を解説します。


目次

SelectManyメソッドの概要

SelectManyは、シーケンスの各要素をIEnumerable<T>に射影し、それらの結果を一つのシーケンスに統合します。

  • Selectの場合: 各要素を変換するため、結果は IEnumerable<string[]>(配列の配列)になります。
  • SelectManyの場合: 各要素の中身を展開して結合するため、結果は IEnumerable<string>(文字列のリスト)になります。

実践的なコード例:全プレイリストからのジャンル抽出

以下のコードは、複数のプレイリストに設定された「ジャンルタグ」をすべて取得し、重複を排除して「システム内で使用されているジャンル一覧」を作成する例です。

using System;
using System.Collections.Generic;
using System.Linq;

namespace MusicStreamingService
{
    // プレイリストを表すレコード
    public record Playlist(string Title, string[] Genres);

    class Program
    {
        static void Main()
        {
            // シナリオ:
            // ユーザーが作成したプレイリストのコレクションがある。
            // 各プレイリストには、複数の「ジャンル」がタグ付けされている。
            // 全てのプレイリストに含まれるジャンルを、重複なく抽出したい。

            var userPlaylists = new[]
            {
                new Playlist("朝のカフェ", new[] { "Jazz", "Bossa Nova", "Lo-Fi" }),
                new Playlist("ドライブ用", new[] { "Rock", "Pop", "EDM" }),
                new Playlist("集中モード", new[] { "Lo-Fi", "Ambient", "Classical" }),
                new Playlist("90年代ヒッツ", new[] { "Pop", "Rock", "R&B" })
            };

            // 1. SelectManyを使用して、各プレイリストのGenres配列を平坦化する
            //    この時点で、全てのジャンルが1つのシーケンスに並びます。
            //    例: "Jazz", "Bossa Nova", ..., "Rock", "Pop", ...
            var flattenedGenres = userPlaylists.SelectMany(p => p.Genres);

            // 2. Distinctを使用して重複を除去し、OrderByで整列する
            var uniqueGenres = flattenedGenres
                .Distinct()
                .OrderBy(g => g);

            // 結果の出力
            Console.WriteLine("--- 抽出されたジャンル一覧 ---");
            Console.WriteLine(string.Join(", ", uniqueGenres));
        }
    }
}

実行結果

--- 抽出されたジャンル一覧 ---
Ambient, Bossa Nova, Classical, EDM, Jazz, Lo-Fi, Pop, R&B, Rock

技術的なポイント

1. SelectとSelectManyの違い

もし上記のコードでSelectを使用した場合、戻り値の型はIEnumerable<string[]>となります。

// Selectの場合
var nestedList = userPlaylists.Select(p => p.Genres);
// 結果: { { "Jazz", ... }, { "Rock", ... }, ... } という「配列のリスト」

これに対してSelectManyは、取得した各配列の「中身」を取り出し、一つの大きな流れ(ストリーム)として結合します。

2. 親要素の情報を保持したい場合

SelectManyには、展開された子要素だけでなく、親要素の情報も同時に扱いたい場合に使えるオーバーロードが存在します。これを利用すると、「どのプレイリストに由来するジャンルか」という情報を保持したまま平坦化できます。

var genreWithSource = userPlaylists.SelectMany(
    playlist => playlist.Genres, // コレクションへの射影
    (playlist, genre) => new { PlaylistTitle = playlist.Title, Genre = genre } // 結果の生成
);

// 出力例: 
// 朝のカフェ - Jazz
// 朝のカフェ - Bossa Nova
// ドライブ用 - Rock
// ...

3. クエリ構文での記述

クエリ構文を使用する場合、SelectManyは2つのfrom句として表現されます。多重ループのような記述になるため、直感的に理解しやすい場合があります。

var query = from p in userPlaylists
            from g in p.Genres
            select g;
// これでSelectMany(p => p.Genres)と同じ結果になります

まとめ

SelectManyは、階層構造を持つデータ(1対多の関係)を、分析や一覧表示のためにフラットな形式に変換する際に不可欠なメソッドです。「リストの中のリスト」を処理する必要が出てきた際は、foreachの二重ループを書く前に、SelectManyで解決できないか検討することをお勧めします。コードのネストが浅くなり、可読性が大幅に向上します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次