C# SelectManyとは?Selectとの違い・入れ子リストの平坦化を初心者向けにコード例で解説

はじめに

C#で複数のリストを扱っていると、次のような入れ子構造のデータを1つのリストにまとめたい場面があります。

C#
var numberGroups = new List<List<int>>
{
new() { 1, 2, 3 },
new() { 4, 5 },
new() { 6, 7, 8 }
};

このデータは、外側のリストの中に複数の整数リストが格納されたList<List<int>>型です。すべての整数を1つのList<int>として取得したい場合は、LINQのSelectManyを使用できます。

C#
List<int> numbers = numberGroups
.SelectMany(group => group)
.ToList();

実行結果は次のとおりです。

1, 2, 3, 4, 5, 6, 7, 8

SelectManyは、それぞれの要素からコレクションを取り出し、それらを連結して1つのシーケンスに平坦化するメソッドです。Microsoft Learnでも、各要素をIEnumerable<T>へ変換し、生成された複数のシーケンスを1つに平坦化するメソッドとして説明されています。Microsoft Learn+1

この記事では、C#のSelectManyの基本構文、Selectとの違い、入れ子リストの平坦化、親要素の情報を残す方法、よくあるエラーについて、初心者向けのコード例を使って解説します。

1. C#のSelectManyとは

1-1. SelectManyは複数のコレクションを1つに平坦化するLINQメソッド

SelectManyは、複数のコレクションを1つの連続したコレクションとして扱うためのLINQメソッドです。

たとえば、次のデータには3つの文字列配列が含まれています。

C#
var groups = new[]
{
new[] { "A", "B" },
new[] { "C", "D" },
new[] { "E", "F" }
};

このデータの型はstring[][]です。SelectManyを使用すると、内側の配列を連結してIEnumerable<string>を取得できます。

C#
IEnumerable<string> values = groups.SelectMany(group => group);

Console.WriteLine(string.Join(", ", values));

実行結果は次のとおりです。

A, B, C, D, E, F

入れ子になった構造を1階層にする操作は、一般に「平坦化」や「フラット化」と呼ばれます。

1-2. SelectManyを使うと何ができるのか

SelectManyを使用すると、主に次のような処理を簡潔に記述できます。

  • 複数のリストを1つのリストにまとめる

  • 親オブジェクトが持つ子コレクションをすべて取得する

  • 部署一覧から全社員を取得する

  • 注文一覧から全商品を取得する

  • 入れ子配列から条件に合う要素だけを抽出する

  • 親と子の情報を組み合わせたデータを作成する

通常のforeachを使っても同じ処理は書けます。

C#
var result = new List<int>();

foreach (var group in numberGroups)
{
foreach (var number in group)
{
result.Add(number);
}
}

SelectManyを使うと、同じ処理を次のように短く表現できます。

C#
var result = numberGroups.SelectMany(group => group).ToList();

単にコードが短くなるだけではなく、「内側のコレクションを取り出して平坦化する」という処理の意図が明確になります。

1-3. SelectManyが役立つ入れ子リストの具体例

ECサイトの注文データを例に考えてみましょう。1件の注文には、複数の商品が含まれています。

C#
public class Order
{
public int OrderId { get; set; }
public List<string> Products { get; set; } = new();
}

注文一覧は次のようなデータです。

C#
var orders = new List<Order>
{
new()
{
OrderId = 1,
Products = new List<string> { "パソコン", "マウス" }
},
new()
{
OrderId = 2,
Products = new List<string> { "キーボード", "モニター" }
}
};

全注文に含まれる商品を取得する場合、SelectManyを使用します。

C#
var allProducts = orders
.SelectMany(order => order.Products)
.ToList();

Console.WriteLine(string.Join(", ", allProducts));

実行結果は次のとおりです。

パソコン, マウス, キーボード, モニター

このように、「親要素ごとに複数の子要素が存在するデータ」を扱う場面でSelectManyが役立ちます。

2. SelectManyの基本的な書き方

2-1. SelectManyの基本構文

SelectManyの基本構文は次のとおりです。

C#
var result = source.SelectMany(element => element.ChildCollection);

各部分の意味は次のようになります。

source
元となるコレクション

element
元のコレクションから順番に取り出される要素

element.ChildCollection
各要素から取得する子コレクション

result
子コレクションを連結して平坦化した結果

たとえば、List<List<int>>を平坦化する場合は次のように書きます。

C#
var result = numberGroups.SelectMany(group => group);

外側のリストから取り出されるgroup自体がList<int>なので、そのまま返すことで、各リストの要素が1つのシーケンスにまとめられます。

SelectManySystem.Linq名前空間の拡張メソッドです。利用できない場合は、ファイルの先頭に次の記述があるか確認しましょう。

C#
using System.Linq;

2-2. ラムダ式の引数と戻り値の意味

次のコードを例に、ラムダ式の意味を確認します。

C#
var employees = departments
.SelectMany(department => department.Employees);

departmentには、departmentsから部署オブジェクトが1つずつ渡されます。

ラムダ式は次の部分です。

C#
department => department.Employees

左側のdepartmentが引数、右側のdepartment.Employeesが戻り値です。

重要なのは、SelectManyに渡すラムダ式が、単一の値ではなくコレクションを返すことです。

C#
// コレクションを返す
department => department.Employees

各部署の社員リストを順番に取得し、それらを連結することで、すべての社員が1つのシーケンスになります。

2-3. メソッド構文とクエリ構文の書き方

LINQには、メソッド構文とクエリ構文があります。

メソッド構文でSelectManyを使用する場合は、次のように記述します。

C#
var employees = departments
.SelectMany(department => department.Employees);

クエリ構文では、複数のfrom句を使用するとSelectManyに相当する処理になります。

C#
var employees =
from department in departments
from employee in department.Employees
select employee;

どちらも、各部署の社員を1つのシーケンスとして取得します。

親と子の情報を同時に取得する場合は、次のように書けます。

C#
var result =
from department in departments
from employee in department.Employees
select new
{
DepartmentName = department.Name,
EmployeeName = employee.Name
};

複数のfrom句を使用するクエリ構文は、内部的にSelectManyへ変換されます。Microsoft Learn+1

2-4. SelectManyの戻り値がIEnumerableになる理由

List<T>に対してSelectManyを実行しても、通常の戻り値はList<T>ではなくIEnumerable<T>です。

C#
IEnumerable<int> result = numberGroups
.SelectMany(group => group);

IEnumerable<T>は、要素を順番に列挙できることを表すインターフェースです。LINQの多くのメソッドは、結果をすぐにリストへ格納せず、必要になったときに要素を列挙できる形で返します。

リストとして使用したい場合は、最後にToListを呼び出します。

C#
List<int> result = numberGroups
.SelectMany(group => group)
.ToList();

配列として取得したい場合はToArrayを使用します。

C#
int[] result = numberGroups
.SelectMany(group => group)
.ToArray();

3. SelectとSelectManyの違い

3-1. Selectは要素を変換しSelectManyは変換後のコレクションを平坦化する

SelectSelectManyは、どちらもLINQの射影操作に使用されますが、結果の構造が異なります。

Selectは、元の各要素を別の要素へ変換します。

C#
var lengths = words.Select(word => word.Length);

一方、SelectManyは、元の各要素をコレクションへ変換し、その複数のコレクションを1つに平坦化します。

C#
var employees = departments
.SelectMany(department => department.Employees);

考え方を簡単に整理すると、次のようになります。

Select
1要素 → 1要素

SelectMany
1要素 → 複数要素
さらに、すべての複数要素を1つに連結

Microsoft Learnでも、Selectは各入力要素から1つの結果を生成し、SelectManyは各入力要素から生成された複数のシーケンスを連結すると説明されています。Microsoft Learn+1

3-2. Selectを使った場合に入れ子構造が残るコード例

次の入れ子リストにSelectを使用してみます。

C#
var groups = new List<List<int>>
{
new() { 1, 2 },
new() { 3, 4 },
new() { 5, 6 }
};

IEnumerable<List<int>> result = groups
.Select(group => group);

Selectは各List<int>をそのまま返すため、結果はIEnumerable<List<int>>です。

各数値を2倍にする場合も、内側でSelectを使用すると入れ子構造が残ります。

C#
IEnumerable<IEnumerable<int>> result = groups
.Select(group => group.Select(number => number * 2));

結果のイメージは次のとおりです。

[
[2, 4],
[6, 8],
[10, 12]
]

外側の要素数は3つのままで、それぞれの要素が数値のコレクションになっています。

3-3. SelectManyを使って1階層に平坦化するコード例

SelectManyを使用すると、各グループの数値を2倍にしてから、1つのシーケンスへ平坦化できます。

C#
IEnumerable<int> result = groups
.SelectMany(group => group.Select(number => number * 2));

Console.WriteLine(string.Join(", ", result));

実行結果は次のとおりです。

2, 4, 6, 8, 10, 12

単に平坦化するだけであれば、次のように書きます。

C#
IEnumerable<int> result = groups
.SelectMany(group => group);

3-4. SelectとSelectManyの使い分け方

元の各要素から1つの結果を作る場合はSelectを使用します。

C#
var employeeNames = employees
.Select(employee => employee.Name);

元の各要素から複数の結果を取り出し、それらを1つにまとめる場合はSelectManyを使用します。

C#
var allEmployees = departments
.SelectMany(department => department.Employees);

結果を入れ子のまま保持したい場合はSelect、1階層にまとめたい場合はSelectManyと考えると判断しやすくなります。

4. SelectManyで入れ子リストを平坦化する方法

4-1. List<List<T>>を1つのList<T>に変換する

List<List<T>>List<T>へ変換する基本例は次のとおりです。

C#
var nestedNumbers = new List<List<int>>
{
new() { 10, 20 },
new() { 30, 40 },
new() { 50, 60 }
};

List<int> numbers = nestedNumbers
.SelectMany(list => list)
.ToList();

Console.WriteLine(string.Join(", ", numbers));

実行結果は次のとおりです。

10, 20, 30, 40, 50, 60

型の変化は次のようになります。

変換前: List<List<int>>
変換後: List<int>

文字列のリストでも同様です。

C#
var nestedWords = new List<List<string>>
{
new() { "赤", "青" },
new() { "緑", "黄" }
};

List<string> words = nestedWords
.SelectMany(list => list)
.ToList();

4-2. 配列の入れ子を平坦化する

配列の入れ子もSelectManyで平坦化できます。

C#
int[][] nestedArray =
{
new[] { 1, 2, 3 },
new[] { 4, 5 },
new[] { 6, 7 }
};

int[] flatArray = nestedArray
.SelectMany(array => array)
.ToArray();

Console.WriteLine(string.Join(", ", flatArray));

実行結果は次のとおりです。

1, 2, 3, 4, 5, 6, 7

SelectManyの直後はIEnumerable<int>ですが、ToArrayを呼び出すことでint[]へ変換しています。

4-3. オブジェクトが持つ子コレクションを平坦化する

実務では、単純な入れ子リストよりも、親オブジェクトが子コレクションを持つ構造でSelectManyを使用することが多くあります。

C#
public class Department
{
public string Name { get; set; } = "";
public List<Employee> Employees { get; set; } = new();
}

public class Employee
{
public string Name { get; set; } = "";
}

部署一覧を作成します。

C#
var departments = new List<Department>
{
new()
{
Name = "営業部",
Employees = new List<Employee>
{
new() { Name = "佐藤" },
new() { Name = "鈴木" }
}
},
new()
{
Name = "開発部",
Employees = new List<Employee>
{
new() { Name = "田中" },
new() { Name = "高橋" }
}
}
};

すべての社員を取得するコードは次のとおりです。

C#
IEnumerable<Employee> employees = departments
.SelectMany(department => department.Employees);

foreach (var employee in employees)
{
Console.WriteLine(employee.Name);
}

実行結果は次のとおりです。

佐藤
鈴木
田中
高橋

4-4. ToListと組み合わせてList型として取得する

SelectManyの結果に対して、要素の追加や削除、インデックスによるアクセスを行いたい場合は、ToListでリストへ変換します。

C#
List<Employee> employees = departments
.SelectMany(department => department.Employees)
.ToList();

ただし、列挙するだけであれば、必ずしもToListは必要ありません。

C#
foreach (var employee in departments.SelectMany(d => d.Employees))
{
Console.WriteLine(employee.Name);
}

不要なToListを何度も呼び出すと、新しいリストを作成するためのメモリと処理時間が必要になります。結果を具体的なリストとして保持する必要があるときに使用しましょう。

5. SelectManyの実践的なコード例

5-1. 部署ごとの社員リストから全社員を取得する

部署一覧から全社員を取得する場合は、各部署のEmployeesプロパティを返します。

C#
List<Employee> allEmployees = departments
.SelectMany(department => department.Employees)
.ToList();

社員名だけが必要な場合は、平坦化した後にSelectを使用します。

C#
List<string> employeeNames = departments
.SelectMany(department => department.Employees)
.Select(employee => employee.Name)
.ToList();

SelectManyの中で名前へ変換することもできます。

C#
List<string> employeeNames = departments
.SelectMany(
department => department.Employees,
(department, employee) => employee.Name)
.ToList();

社員だけを取得するなら、最初の書き方のほうが読みやすいでしょう。

5-2. 注文一覧からすべての商品を取得する

商品を表すクラスを定義します。

C#
public class Product
{
public string Name { get; set; } = "";
public int Price { get; set; }
}

public class Order
{
public int OrderId { get; set; }
public List<Product> Products { get; set; } = new();
}

注文一覧からすべての商品を取得します。

C#
List<Product> allProducts = orders
.SelectMany(order => order.Products)
.ToList();

商品名だけを取得する場合はSelectを続けます。

C#
List<string> productNames = orders
.SelectMany(order => order.Products)
.Select(product => product.Name)
.ToList();

5-3. SelectManyとWhereを組み合わせて条件に合う要素を抽出する

SelectManyで平坦化した結果にWhereを適用すると、すべての子要素から条件に合うものだけを抽出できます。

たとえば、価格が1万円以上の商品を取得するコードは次のとおりです。

C#
List<Product> expensiveProducts = orders
.SelectMany(order => order.Products)
.Where(product => product.Price >= 10_000)
.ToList();

子コレクションごとに絞り込んでから平坦化することもできます。

C#
List<Product> expensiveProducts = orders
.SelectMany(order =>
order.Products.Where(product => product.Price >= 10_000))
.ToList();

どちらでも同じ結果を得られます。単純な条件であれば、平坦化後にWhereを記述するほうが処理の流れを理解しやすいでしょう。

5-4. SelectManyとDistinctを組み合わせて重複を除外する

複数のグループに同じ値が含まれている場合は、Distinctで重複を除外できます。

C#
var skillGroups = new List<List<string>>
{
new() { "C#", "SQL", "JavaScript" },
new() { "C#", "Python", "SQL" },
new() { "Java", "C#" }
};

List<string> uniqueSkills = skillGroups
.SelectMany(skills => skills)
.Distinct()
.ToList();

Console.WriteLine(string.Join(", ", uniqueSkills));

実行結果は次のとおりです。

C#, SQL, JavaScript, Python, Java

オブジェクトの特定プロパティを基準に重複を除外したい場合は、利用している.NETのバージョンに応じてDistinctByを使用できます。

C#
List<Product> uniqueProducts = orders
.SelectMany(order => order.Products)
.DistinctBy(product => product.Name)
.ToList();

5-5. SelectManyとGroupByを組み合わせて集計する

SelectManyで平坦化した要素をGroupByで分類し、集計することもできます。

次の例では、全注文の商品を商品名ごとにまとめ、件数を数えています。

C#
var productCounts = orders
.SelectMany(order => order.Products)
.GroupBy(product => product.Name)
.Select(group => new
{
ProductName = group.Key,
Count = group.Count()
})
.ToList();

foreach (var item in productCounts)
{
Console.WriteLine($"{item.ProductName}: {item.Count}件");
}

価格の合計を商品名ごとに集計する場合は、次のように書けます。

C#
var totals = orders
.SelectMany(order => order.Products)
.GroupBy(product => product.Name)
.Select(group => new
{
ProductName = group.Key,
TotalPrice = group.Sum(product => product.Price)
})
.ToList();

6. 親要素の情報を残したままSelectManyで平坦化する方法

6-1. SelectManyのresultSelectorとは

通常のSelectManyで子コレクションを平坦化すると、結果には子要素だけが残ります。

C#
var employees = departments
.SelectMany(department => department.Employees);

この結果からは、それぞれの社員がどの部署に所属していたのかを直接判断できません。

親要素の情報を残したい場合は、resultSelectorを受け取るオーバーロードを使用できます。

C#
var result = source.SelectMany(
parent => parent.Children,
(parent, child) => new
{
Parent = parent,
Child = child
});

第1引数では子コレクションを指定し、第2引数では親要素と子要素から最終結果を作成します。

6-2. 親オブジェクトと子オブジェクトを同時に取得する

部署と社員を同時に取得する例は次のとおりです。

C#
var result = departments.SelectMany(
department => department.Employees,
(department, employee) => new
{
Department = department,
Employee = employee
});

利用時には、親と子の両方へアクセスできます。

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

実行結果は次のとおりです。

営業部: 佐藤
営業部: 鈴木
開発部: 田中
開発部: 高橋

6-3. 匿名型へ変換して必要なプロパティだけを返す

親オブジェクトと子オブジェクトを丸ごと保持する必要がない場合は、必要なプロパティだけを匿名型へ変換できます。

C#
var result = departments.SelectMany(
department => department.Employees,
(department, employee) => new
{
DepartmentName = department.Name,
EmployeeName = employee.Name
});

結果には部署名と社員名だけが含まれます。

C#
foreach (var item in result)
{
Console.WriteLine(
$"{item.DepartmentName}: {item.EmployeeName}");
}

メソッドの戻り値として利用する場合は、匿名型ではなく専用のDTOやレコードを作成すると扱いやすくなります。

C#
public class EmployeeDepartmentDto
{
public string DepartmentName { get; set; } = "";
public string EmployeeName { get; set; } = "";
}
C#
List<EmployeeDepartmentDto> result = departments.SelectMany(
department => department.Employees,
(department, employee) => new EmployeeDepartmentDto
{
DepartmentName = department.Name,
EmployeeName = employee.Name
})
.ToList();

6-4. resultSelectorを使わずSelectを組み合わせる書き方

resultSelectorを使わず、内側のSelectで親と子を組み合わせることもできます。

C#
var result = departments
.SelectMany(department =>
department.Employees.Select(employee => new
{
DepartmentName = department.Name,
EmployeeName = employee.Name
}));

次のresultSelectorを使ったコードと同じ結果になります。

C#
var result = departments.SelectMany(
department => department.Employees,
(department, employee) => new
{
DepartmentName = department.Name,
EmployeeName = employee.Name
});

どちらを使用しても問題ありません。チームのコーディング規約や、読みやすいと感じる書き方に合わせて選択しましょう。

7. SelectManyを使う際の注意点

7-1. 子コレクションがnullの場合に例外が発生する

SelectManyで取得する子コレクションがnullの場合、列挙時に例外が発生します。

C#
public class Department
{
public string Name { get; set; } = "";
public List<Employee>? Employees { get; set; }
}

次のデータでは、営業部のEmployeesnullです。

C#
var departments = new List<Department>
{
new()
{
Name = "営業部",
Employees = null
},
new()
{
Name = "開発部",
Employees = new List<Employee>
{
new() { Name = "田中" }
}
}
};

そのまま平坦化すると問題が発生します。

C#
var employees = departments
.SelectMany(department => department.Employees!);

外側のコレクション自体がnullの場合も、SelectManyを呼び出せません。

7-2. nullを空のコレクションとして扱う方法

子コレクションがnullになる可能性がある場合は、Enumerable.Empty<T>()を使用して空のコレクションへ置き換えます。

C#
var employees = departments
.SelectMany(department =>
department.Employees ?? Enumerable.Empty<Employee>());

配列を使って空のコレクションを表すこともできます。

C#
var employees = departments
.SelectMany(department =>
department.Employees ?? Array.Empty<Employee>());

コレクション式を利用できる環境では、次のように書くこともできます。

C#
var employees = departments
.SelectMany(department => department.Employees ?? []);

可能であれば、クラスのプロパティを最初から空のリストで初期化しておく方法が安全です。

C#
public List<Employee> Employees { get; set; } = new();

7-3. 遅延実行によって処理が実行されるタイミング

SelectManyは、通常、呼び出した時点ではすべての要素を処理しません。foreachで列挙したり、ToListToArrayを呼び出したりした時点で処理されます。

C#
var query = departments
.SelectMany(department => department.Employees);

// この時点で結果をリストとして確定する
var employees = query.ToList();

クエリを作った後、列挙する前に元のコレクションを変更すると、その変更が結果に反映される場合があります。

C#
var query = departments
.SelectMany(department => department.Employees);

departments[0].Employees.Add(
new Employee { Name = "山田" });

var employees = query.ToList();

結果をその時点の状態で確定したい場合は、早い段階でToListを呼び出します。

C#
var employees = departments
.SelectMany(department => department.Employees)
.ToList();

LINQのEnumerableに属する多くの演算子は、列挙されるまで処理を開始しない遅延実行の性質を持ちます。Microsoft Learn+1

7-4. 大量データを平坦化する際のパフォーマンス

SelectManyは、内側のコレクションを順番に列挙します。処理対象の要素数が多ければ、それに応じて処理時間も増えます。

たとえば、100個の親要素がそれぞれ1,000個の子要素を持つ場合、平坦化後には最大10万個の要素を処理します。

大量データを扱う場合は、次の点を意識しましょう。

  • 不要なToListを途中で何度も呼ばない

  • 可能であれば早い段階でWhereによる絞り込みを行う

  • 同じクエリを何度も列挙しない

  • 必要なプロパティだけをSelectする

  • データベースでは生成されるSQLも確認する

  • 全件をメモリへ読み込む必要があるか検討する

次のコードでは、すべての商品を平坦化した後に条件を適用しています。

C#
var result = orders
.SelectMany(order => order.Products)
.Where(product => product.Price >= 10_000);

注文側にも条件がある場合は、先に対象の注文を絞ると、列挙する子要素を減らせる可能性があります。

C#
var result = orders
.Where(order => order.OrderId >= 100)
.SelectMany(order => order.Products)
.Where(product => product.Price >= 10_000);

7-5. 多階層の入れ子はSelectManyを複数回使う

3階層以上の入れ子を平坦化する場合は、SelectManyを複数回使用します。

C#
var data = new List<List<List<int>>>
{
new()
{
new() { 1, 2 },
new() { 3, 4 }
},
new()
{
new() { 5, 6 },
new() { 7, 8 }
}
};

1回目のSelectManyでは、3階層から2階層になります。

C#
IEnumerable<List<int>> twoLevels = data
.SelectMany(level2 => level2);

さらにSelectManyを使用すると、1階層になります。

C#
List<int> flat = data
.SelectMany(level2 => level2)
.SelectMany(level1 => level1)
.ToList();

次のように内側へまとめることもできます。

C#
List<int> flat = data
.SelectMany(level2 =>
level2.SelectMany(level1 => level1))
.ToList();

8. SelectManyでよくあるエラーと解決方法

8-1. 型を推論できないエラーが発生する場合

SelectManyに渡すラムダ式は、列挙可能なコレクションを返す必要があります。分岐によって異なる型を返すと、コンパイラが型を推論できないことがあります。

たとえば、次のように片方でリスト、もう片方でnullを返すコードは避けましょう。

C#
var result = departments.SelectMany(department =>
department.Name == "開発部"
? department.Employees
: null);

空のコレクションを返すように修正します。

C#
var result = departments.SelectMany(department =>
department.Name == "開発部"
? department.Employees
: Enumerable.Empty<Employee>());

型が分かりにくい場合は、戻り値の型を明示する方法もあります。

C#
var result = departments.SelectMany(
(Department department) =>
department.Employees ?? Enumerable.Empty<Employee>());

8-2. SelectManyを使っても期待した型にならない場合

SelectManyを使用したのに、期待していた型と異なる場合は、ラムダ式が何を返しているか確認しましょう。

次のコードでは、各社員の名前を返しています。

C#
IEnumerable<string> result = departments
.SelectMany(department =>
department.Employees.Select(employee => employee.Name));

そのため、結果はIEnumerable<Employee>ではなくIEnumerable<string>です。

社員オブジェクトが必要なら、社員コレクションをそのまま返します。

C#
IEnumerable<Employee> result = departments
.SelectMany(department => department.Employees);

また、SelectManyの戻り値は通常IEnumerable<T>です。List<T>が必要な場合はToListを呼び出します。

C#
List<Employee> result = departments
.SelectMany(department => department.Employees)
.ToList();

8-3. nullを含むコレクションでNullReferenceExceptionが発生する場合

子コレクションにnullが含まれている場合は、空のコレクションへ置き換えます。

C#
var result = departments
.SelectMany(department =>
department.Employees ?? Enumerable.Empty<Employee>());

外側のコレクション自体がnullになる可能性がある場合も対策が必要です。

C#
List<Department>? departments = GetDepartments();

var result = (departments ?? new List<Department>())
.SelectMany(department =>
department.Employees ?? Enumerable.Empty<Employee>())
.ToList();

次のようにEnumerable.Empty<T>()を使用しても構いません。

C#
var result = (departments ?? Enumerable.Empty<Department>())
.SelectMany(department =>
department.Employees ?? Enumerable.Empty<Employee>())
.ToList();

8-4. 文字列にSelectManyを使うと文字単位に展開される場合

C#のstringIEnumerable<char>として列挙できます。そのため、文字列のリストにSelectManyを使用すると、文字単位に平坦化されます。

C#
var words = new List<string>
{
"CSharp",
"LINQ"
};

IEnumerable<char> characters = words
.SelectMany(word => word);

Console.WriteLine(string.Join(", ", characters));

実行結果は次のようになります。

C, S, h, a, r, p, L, I, N, Q

文字列を文字列のまま取得したい場合、単純な一覧であればSelectManyは必要ありません。

C#
IEnumerable<string> result = words;

文字列を複数の単語へ分割して平坦化したい場合は、Splitと組み合わせます。

C#
var sentences = new[]
{
"C# LINQ",
"Select SelectMany"
};

string[] words = sentences
.SelectMany(sentence =>
sentence.Split(' ', StringSplitOptions.RemoveEmptyEntries))
.ToArray();

8-5. 平坦化後に元の親要素が分からなくなる場合

次のコードでは、平坦化後に社員情報だけが残ります。

C#
var employees = departments
.SelectMany(department => department.Employees);

所属部署も必要であれば、resultSelectorで親の情報を残します。

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

または、内側のSelectで親と子を組み合わせます。

C#
var result = departments.SelectMany(department =>
department.Employees.Select(employee => new
{
DepartmentName = department.Name,
Employee = employee
}));

平坦化を始める前に、最終結果で親情報が必要かどうかを確認しておくことが大切です。

9. SelectManyに関するよくある質問

9-1. SelectManyとJoinの違いは何ですか

SelectManyは、ある要素が持つ子コレクションを取り出して平坦化するために使用します。

C#
var employees = departments
.SelectMany(department => department.Employees);

Joinは、2つの独立したコレクションを共通のキーで関連付けるために使用します。

C#
var result = employees.Join(
departments,
employee => employee.DepartmentId,
department => department.Id,
(employee, department) => new
{
EmployeeName = employee.Name,
DepartmentName = department.Name
});

使い分けの目安は次のとおりです。

親オブジェクトから子コレクションをたどれる
→ SelectMany

別々のコレクションをキーで関連付ける
→ Join

Joinは、オブジェクト間を直接たどれないデータをキーで関連付ける用途に適しています。Microsoft Learn

9-2. SelectManyとSelect+SelectManyはどう使い分けますか

単に子コレクションを平坦化する場合は、SelectManyだけで十分です。

C#
var employees = departments
.SelectMany(department => department.Employees);

事前に親要素を別の形へ変換する必要がある場合は、SelectSelectManyを組み合わせることがあります。

C#
var employeeNames = departments
.Select(department => department.Employees)
.SelectMany(employees => employees)
.Select(employee => employee.Name);

ただし、この例は次のように簡潔に書けます。

C#
var employeeNames = departments
.SelectMany(department => department.Employees)
.Select(employee => employee.Name);

Selectを先に使う明確な理由がなければ、直接SelectManyを使用するほうが読みやすくなります。

親と子を同時に変換したい場合は、内側のSelectを組み合わせる書き方も便利です。

C#
var result = departments.SelectMany(department =>
department.Employees.Select(employee => new
{
DepartmentName = department.Name,
EmployeeName = employee.Name
}));

9-3. SelectManyで3階層以上のリストを平坦化できますか

3階層以上でも平坦化できます。階層の数に応じてSelectManyを複数回使用します。

C#
List<List<List<int>>> nested = new()
{
new()
{
new() { 1, 2 },
new() { 3, 4 }
},
new()
{
new() { 5, 6 }
}
};

List<int> result = nested
.SelectMany(level2 => level2)
.SelectMany(level1 => level1)
.ToList();

実行結果は次のとおりです。

1, 2, 3, 4, 5, 6

ただし、階層の深さが実行時まで分からない再帰的なデータ構造では、固定回数のSelectManyでは対応できません。その場合は再帰処理を検討します。

9-4. SelectManyの結果を配列やリストに変換できますか

ToListを使用するとリストへ変換できます。

C#
List<int> list = nestedNumbers
.SelectMany(numbers => numbers)
.ToList();

ToArrayを使用すると配列へ変換できます。

C#
int[] array = nestedNumbers
.SelectMany(numbers => numbers)
.ToArray();

ほかにも、キーが重複しないデータであればToDictionaryを使用できます。

C#
Dictionary<int, Product> productMap = orders
.SelectMany(order => order.Products)
.ToDictionary(product => product.Id);

同じキーが複数存在するとToDictionaryで例外が発生するため、必要に応じてDistinctByGroupByで整理してから変換しましょう。

9-5. SelectManyはDictionaryにも使用できますか

Dictionary<TKey, TValue>も列挙可能なので、SelectManyを使用できます。列挙時の各要素はKeyValuePair<TKey, TValue>です。

値がリストになっている辞書を例にします。

C#
var employeesByDepartment =
new Dictionary<string, List<string>>
{
["営業部"] = new List<string> { "佐藤", "鈴木" },
["開発部"] = new List<string> { "田中", "高橋" }
};

すべての社員名を取得する場合は、各要素のValueを平坦化します。

C#
List<string> employees = employeesByDepartment
.SelectMany(pair => pair.Value)
.ToList();

部署名も残したい場合は、resultSelectorを使用します。

C#
var result = employeesByDepartment.SelectMany(
pair => pair.Value,
(pair, employeeName) => new
{
DepartmentName = pair.Key,
EmployeeName = employeeName
});

内側のSelectを使っても同じ処理を記述できます。

C#
var result = employeesByDepartment.SelectMany(pair =>
pair.Value.Select(employeeName => new
{
DepartmentName = pair.Key,
EmployeeName = employeeName
}));

まとめ

C#のSelectManyは、複数の子コレクションを取り出し、1つのシーケンスへ平坦化するLINQメソッドです。

Selectは各要素を別の要素へ変換しますが、変換後の結果がコレクションであっても入れ子構造を維持します。一方、SelectManyは各要素から取得したコレクションを連結するため、List<List<T>>などの入れ子構造をIEnumerable<T>として扱えます。

基本的な書き方は次のとおりです。

C#
var result = source
.SelectMany(parent => parent.Children);

リストとして取得する場合はToListを組み合わせます。

C#
List<Child> result = source
.SelectMany(parent => parent.Children)
.ToList();

親要素の情報を残したい場合は、resultSelectorまたは内側のSelectを使用します。

C#
var result = source.SelectMany(
parent => parent.Children,
(parent, child) => new
{
Parent = parent,
Child = child
});

SelectManyを使用する際は、子コレクションのnull、遅延実行、不要なToList、文字列が文字単位に展開される点にも注意が必要です。

「各要素が持つ複数の子要素を、すべてまとめて扱いたい」と考えたときは、SelectManyを利用できないか検討してみましょう。