C# の LINQ で平坦化する SelectMany

SelectMany はネストしたコレクションを平坦化するメソッドだ。Select が各要素を1対1で変換するのに対し、SelectMany は各要素から複数の要素を生成し、それらをすべて一つのシーケンスにまとめる。

Select との違い

まず SelectSelectMany の違いを見てみよう。

var sentences = new List<string> { "Hello World", "Good Morning" };

// Select: 配列の配列が返る
var withSelect = sentences.Select(s => s.Split(' '));
// 結果: [["Hello", "World"], ["Good", "Morning"]]

// SelectMany: 平坦化される
var withSelectMany = sentences.SelectMany(s => s.Split(' '));
// 結果: ["Hello", "World", "Good", "Morning"]

Select は各文字列を単語の配列に変換するので、結果は配列の配列になる。一方 SelectMany はすべての単語を一つのシーケンスにまとめる。

Select

各要素を変換。結果の構造が深くなることがある。

SelectMany

各要素から複数の要素を生成し、平坦化。ネストを解消できる。

基本的な使い方

オブジェクトが持つコレクションプロパティを展開するのが典型的な用途だ。

var departments = new List<Department>
{
    new Department 
    { 
        Name = "Engineering", 
        Employees = new List<string> { "Alice", "Bob" }
    },
    new Department 
    { 
        Name = "Sales", 
        Employees = new List<string> { "Charlie", "Diana", "Eve" }
    }
};

// 全社員を一つのリストに
var allEmployees = departments.SelectMany(d => d.Employees);

foreach (var employee in allEmployees)
{
    Console.WriteLine(employee);
}
// Alice, Bob, Charlie, Diana, Eve

各部署が持つ従業員リストを、部署の壁を取り払って一つのリストにしている。

親要素の情報を保持する

平坦化しつつ、どの親から来たかの情報を残したい場合がある。オーバーロードを使えば可能だ。

var departments = new List<Department>
{
    new Department 
    { 
        Name = "Engineering", 
        Employees = new List<string> { "Alice", "Bob" }
    },
    new Department 
    { 
        Name = "Sales", 
        Employees = new List<string> { "Charlie", "Diana" }
    }
};

var result = departments.SelectMany(
    d => d.Employees,
    (department, employee) => new { department.Name, Employee = employee }
);

foreach (var item in result)
{
    Console.WriteLine($"{item.Name}: {item.Employee}");
}
// Engineering: Alice
// Engineering: Bob
// Sales: Charlie
// Sales: Diana

第二引数のラムダ式で、親要素(department)と子要素(employee)を組み合わせた新しいオブジェクトを作れる。

多段階のネスト解消

二重にネストしたコレクションも、SelectMany を連鎖させれば平坦化できる。

var matrix = new int[][]
{
    new int[] { 1, 2, 3 },
    new int[] { 4, 5, 6 },
    new int[] { 7, 8, 9 }
};

var flattened = matrix.SelectMany(row => row);

foreach (var n in flattened)
{
    Console.Write($"{n} ");  // 1 2 3 4 5 6 7 8 9
}

二次元配列を一次元に展開している。三次元以上なら SelectMany をさらに重ねればいい。

クエリ構文での表現

クエリ構文では、複数の from を使うことで SelectMany と同じことができる。

var departments = new List<Department>
{
    new Department 
    { 
        Name = "Engineering", 
        Employees = new List<string> { "Alice", "Bob" }
    },
    new Department 
    { 
        Name = "Sales", 
        Employees = new List<string> { "Charlie" }
    }
};

// クエリ構文
var result = from d in departments
             from e in d.Employees
             select new { d.Name, Employee = e };

foreach (var item in result)
{
    Console.WriteLine($"{item.Name}: {item.Employee}");
}

2つ目の fromSelectMany に相当する。この書き方の方が直感的に感じる人もいるだろう。

実践的な例:タグの集計

ブログ記事が複数のタグを持つ場合、全タグを集計するのに使える。

var posts = new List<Post>
{
    new Post { Title = "C#入門", Tags = new[] { "csharp", "programming" } },
    new Post { Title = "LINQ解説", Tags = new[] { "csharp", "linq" } },
    new Post { Title = "ASP.NET", Tags = new[] { "csharp", "web", "aspnet" } }
};

var allTags = posts
    .SelectMany(p => p.Tags)
    .Distinct()
    .OrderBy(t => t);

foreach (var tag in allTags)
{
    Console.WriteLine(tag);
}
// aspnet, csharp, linq, programming, web

各記事のタグを展開し、Distinct で重複を除き、アルファベット順に並べている。SelectMany なしでは、この処理は複雑なネストになってしまう。