データのグループ化 (C#)
グループ化とは、各グループの要素が共通の属性を持つようにデータをグループに分ける操作を指します。 次の図は、文字のシーケンスをグループ化した結果を示しています。 各グループのキーは文字です。
重要
これらのサンプルでは、System.Collections.Generic.IEnumerable<T> データ ソースを使用します。 System.Linq.IQueryProvider に基づくデータ ソースでは、System.Linq.IQueryable<T> データ ソースと式ツリーが使用されます。 式ツリーには、許可される C# 構文に制限があります。 さらに、EF Core などの各 IQueryProvider
データ ソースでは、より多くの制限が課される場合があります。 ご利用のデータ ソースのドキュメントをご覧ください。
次の表に、データ要素をグループ化する標準クエリ演算子メソッドの一覧を示しています。
メソッド名 | 説明 | C# のクエリ式の構文 | 説明 |
---|---|---|---|
GroupBy | 共通の属性を共有する要素をグループ化します。 IGrouping<TKey,TElement> オブジェクトは各グループを表します。 | group … by または group … by … into … |
Enumerable.GroupBy Queryable.GroupBy |
ToLookup | キー セレクター関数に基づいて、Lookup<TKey,TElement> (一対多の辞書) に要素を挿入します。 | 該当なし。 | Enumerable.ToLookup |
次のコード例では、group by
句を使用して、リスト内の整数を偶数か奇数かによってグループ化します。
List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
IEnumerable<IGrouping<int, int>> query = from number in numbers
group number by number % 2;
foreach (var group in query)
{
Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
foreach (int i in group)
{
Console.WriteLine(i);
}
}
次のコードでは、メソッド構文を使用した同等のクエリを示しています。
List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
IEnumerable<IGrouping<int, int>> query = numbers
.GroupBy(number => number % 2);
foreach (var group in query)
{
Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
foreach (int i in group)
{
Console.WriteLine(i);
}
}
この記事の次の例では、この分野の一般的なデータ ソースが使用されています。
public enum GradeLevel
{
FirstYear = 1,
SecondYear,
ThirdYear,
FourthYear
};
public class Student
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public required int ID { get; init; }
public required GradeLevel Year { get; init; }
public required List<int> Scores { get; init; }
public required int DepartmentID { get; init; }
}
public class Teacher
{
public required string First { get; init; }
public required string Last { get; init; }
public required int ID { get; init; }
public required string City { get; init; }
}
public class Department
{
public required string Name { get; init; }
public int ID { get; init; }
public required int TeacherID { get; init; }
}
各 Student
は、学年、主要学科、一連のスコアを持っています。 Teacher
は、その教師が授業を受け持つキャンパスを示す City
プロパティも持っています。 Department
には名称と、学科長を務める Teacher
への参照があります。
クエリ結果のグループ化
グループ化は、LINQ の最も強力な機能の 1 つです。 次の例では、さまざまな方法でデータをグループ化する方法を示します。
- 1 つのプロパティで。
- 文字列プロパティの最初の文字で。
- 計算された数値の範囲で。
- ブール述語またはその他の式で。
- 複合キーで。
さらに、最後の 2 つのクエリは、学生の名と姓だけを含む新しい匿名型に結果を射影します。 詳しくは、「group 句」をご覧ください。
1 つのプロパティでグループ化の例
次の例では、要素の 1 つのプロパティをグループ化キーとして使って、ソース要素をグループ化する方法を示します。 キーは enum
で、学生の学年です。 グループ化操作では、型の既定の等値比較子を使います。
var groupByYearQuery =
from student in students
group student by student.Year into newGroup
orderby newGroup.Key
select newGroup;
foreach (var yearGroup in groupByYearQuery)
{
Console.WriteLine($"Key: {yearGroup.Key}");
foreach (var student in yearGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
次の例では、メソッド構文を使用した同等のコードを示しています。
// Variable groupByLastNamesQuery is an IEnumerable<IGrouping<string,
// DataClass.Student>>.
var groupByYearQuery = students
.GroupBy(student => student.Year)
.OrderBy(newGroup => newGroup.Key);
foreach (var yearGroup in groupByYearQuery)
{
Console.WriteLine($"Key: {yearGroup.Key}");
foreach (var student in yearGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
値でグループ化の例
次の例では、オブジェクトのプロパティ以外の何かをグループ化キーとして使って、ソース要素をグループ化する方法を示します。 この例では、キーは学生の姓の最初の文字です。
var groupByFirstLetterQuery =
from student in students
let firstLetter = student.LastName[0]
group student by firstLetter;
foreach (var studentGroup in groupByFirstLetterQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
グループ項目にアクセスするには、入れ子になった foreach が必要です。
次の例では、メソッド構文を使用した同等のコードを示しています。
var groupByFirstLetterQuery = students
.GroupBy(student => student.LastName[0]);
foreach (var studentGroup in groupByFirstLetterQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
}
}
範囲でグループ化の例
次の例では、数値範囲をグループ化キーとして使って、ソース要素をグループ化する方法を示します。 クエリは、名と姓および学生が属しているパーセンタイル範囲のみを含む匿名型に、結果を射影します。 匿名型を使うのは、結果を表示するのに完全な Student
オブジェクトを使う必要がないためです。 GetPercentile
は、学生の平均スコアに基づいてパーセンタイルを計算するヘルパー関数です。 メソッドは、0 から 10 の間の整数を返します。
static int GetPercentile(Student s)
{
double avg = s.Scores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}
var groupByPercentileQuery =
from student in students
let percentile = GetPercentile(student)
group new
{
student.FirstName,
student.LastName
} by percentile into percentGroup
orderby percentGroup.Key
select percentGroup;
foreach (var studentGroup in groupByPercentileQuery)
{
Console.WriteLine($"Key: {studentGroup.Key * 10}");
foreach (var item in studentGroup)
{
Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
}
}
グループとグループ項目を反復処理するには、入れ子になった foreach が必要です。 次の例では、メソッド構文を使用した同等のコードを示しています。
static int GetPercentile(Student s)
{
double avg = s.Scores.Average();
return avg > 0 ? (int)avg / 10 : 0;
}
var groupByPercentileQuery = students
.Select(student => new { student, percentile = GetPercentile(student) })
.GroupBy(student => student.percentile)
.Select(percentGroup => new
{
percentGroup.Key,
Students = percentGroup.Select(s => new { s.student.FirstName, s.student.LastName })
})
.OrderBy(percentGroup => percentGroup.Key);
foreach (var studentGroup in groupByPercentileQuery)
{
Console.WriteLine($"Key: {studentGroup.Key * 10}");
foreach (var item in studentGroup.Students)
{
Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
}
}
比較でグループ化の例
次の例では、ブール比較式を使って、ソース要素をグループ化する方法を示します。 この例のブール式は、学生の平均試験スコアが 75 より大きいかどうかをテストします。 前の例と同じく、完全なソース要素が必要ないため、結果を匿名型に射影します。 匿名型のプロパティは、Key
メンバーのプロパティになります。
var groupByHighAverageQuery =
from student in students
group new
{
student.FirstName,
student.LastName
} by student.Scores.Average() > 75 into studentGroup
select studentGroup;
foreach (var studentGroup in groupByHighAverageQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup)
{
Console.WriteLine($"\t{student.FirstName} {student.LastName}");
}
}
次のコードでは、メソッド構文を使用した同等のクエリを示しています。
var groupByHighAverageQuery = students
.GroupBy(student => student.Scores.Average() > 75)
.Select(group => new
{
group.Key,
Students = group.AsEnumerable().Select(s => new { s.FirstName, s.LastName })
});
foreach (var studentGroup in groupByHighAverageQuery)
{
Console.WriteLine($"Key: {studentGroup.Key}");
foreach (var student in studentGroup.Students)
{
Console.WriteLine($"\t{student.FirstName} {student.LastName}");
}
}
匿名型でグループ化
次の例では、匿名型を使って、複数の値を含むキーをカプセル化する方法を示します。 この例では、最初のキーの値は学生の姓の最初の文字です。 2 番目のキーの値は、最初の試験での学生のスコアが 85 より高いかどうかを示すブール値です。 キーの任意のプロパティでグループを並べ替えることができます。
var groupByCompoundKey =
from student in students
group student by new
{
FirstLetterOfLastName = student.LastName[0],
IsScoreOver85 = student.Scores[0] > 85
} into studentGroup
orderby studentGroup.Key.FirstLetterOfLastName
select studentGroup;
foreach (var scoreGroup in groupByCompoundKey)
{
var s = scoreGroup.Key.IsScoreOver85 ? "more than 85" : "less than 85";
Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetterOfLastName} who scored {s}");
foreach (var item in scoreGroup)
{
Console.WriteLine($"\t{item.FirstName} {item.LastName}");
}
}
次のコードでは、メソッド構文を使用した同等のクエリを示しています。
var groupByCompoundKey = students
.GroupBy(student => new
{
FirstLetterOfLastName = student.LastName[0],
IsScoreOver85 = student.Scores[0] > 85
})
.OrderBy(studentGroup => studentGroup.Key.FirstLetterOfLastName);
foreach (var scoreGroup in groupByCompoundKey)
{
var s = scoreGroup.Key.IsScoreOver85 ? "more than 85" : "less than 85";
Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetterOfLastName} who scored {s}");
foreach (var item in scoreGroup)
{
Console.WriteLine($"\t{item.FirstName} {item.LastName}");
}
}
入れ子になったグループの作成
LINQ クエリ式で入れ子になったグループを作成する方法を次の例に示します。 学年レベルに基づいて作成した各グループを、さらに個人の名前に基づくグループに分割します。
var nestedGroupsQuery =
from student in students
group student by student.Year into newGroup1
from newGroup2 in
from student in newGroup1
group student by student.LastName
group newGroup2 by newGroup1.Key;
foreach (var outerGroup in nestedGroupsQuery)
{
Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
foreach (var innerGroup in outerGroup)
{
Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
foreach (var innerGroupElement in innerGroup)
{
Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
}
}
}
入れ子になったグループの内部要素に対して反復処理を実行するには、入れ子になった foreach
ループが 3 つ必要です。
(反復変数 outerGroup
、innerGroup
、innerGroupElement
の上にマウス カーソルを重ねると、実際の型が表示されます)。
次のコードでは、メソッド構文を使用した同等のクエリを示しています。
var nestedGroupsQuery =
students
.GroupBy(student => student.Year)
.Select(newGroup1 => new
{
newGroup1.Key,
NestedGroup = newGroup1
.GroupBy(student => student.LastName)
});
foreach (var outerGroup in nestedGroupsQuery)
{
Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
foreach (var innerGroup in outerGroup.NestedGroup)
{
Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
foreach (var innerGroupElement in innerGroup)
{
Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
}
}
}
グループ化操作でのサブクエリの実行
この記事では、ソース データを複数のグループに整理し、各グループに対して個別にサブクエリを実行するクエリを作成する方法を 2 つ紹介します。 各例の基本的な手法として、ソース要素のグループ化には、newGroup
という名前の "継続" を使用し、newGroup
に対する新しいサブクエリを生成します。 このサブクエリは、外部クエリによって作成された新しい各グループに対して実行されます。 特にこの例では、最終出力はグループではなく、匿名型のフラットなシーケンスとなります。
グループ化する方法の詳細については、「group 句」を参照してください。 継続の詳細については、「into」を参照してください。 次の例では、インメモリ データ構造をデータ ソースとして使用していますが、どの種類の LINQ データ ソースにも同じ原則が当てはまります。
var queryGroupMax =
from student in students
group student by student.Year into studentGroup
select new
{
Level = studentGroup.Key,
HighestScore = (
from student2 in studentGroup
select student2.Scores.Average()
).Max()
};
var count = queryGroupMax.Count();
Console.WriteLine($"Number of groups = {count}");
foreach (var item in queryGroupMax)
{
Console.WriteLine($" {item.Level} Highest Score={item.HighestScore}");
}
上記のスニペットのクエリは、メソッド構文を使用して記述することもできます。 次のコード スニペットは、メソッド構文を使用して記述した意味的に同等のクエリです。
var queryGroupMax =
students
.GroupBy(student => student.Year)
.Select(studentGroup => new
{
Level = studentGroup.Key,
HighestScore = studentGroup.Max(student2 => student2.Scores.Average())
});
var count = queryGroupMax.Count();
Console.WriteLine($"Number of groups = {count}");
foreach (var item in queryGroupMax)
{
Console.WriteLine($" {item.Level} Highest Score={item.HighestScore}");
}
関連項目
.NET