分組資料 (C#)

分組指的是將資料放在群組中,好讓每一個群組中的項目共用共同的屬性。 下圖顯示一系列字元的分組結果。 每個群組的索引鍵是字元。

Diagram that shows a LINQ Grouping operation

分組資料元素的標準查詢運算子方法詳列於以下表格。

方法名稱 描述 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 最強大的功能之一。 下例示範如何以各種方式分組資料︰

  • 根據單一屬性。
  • 根據字串屬性的第一個字母。
  • 根據計算的數字範圍。
  • 根據布林值述詞或其他運算式。
  • 根據複合索引鍵。

此外,最後兩個查詢會將其結果投射至只包含學生姓名的新匿名型別。 如需詳細資訊,請參閱 group 子句

依單一屬性分組的範例

下例示範如何將項目的單一屬性用為群組索引鍵,來分組來源項目。 該索引鍵是一個 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 是根據學生平均分數計算百分位數的 Helper 函式。 方法會傳回 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}");
    }
}

依匿名型別分組

下例示範如何使用匿名型別來封裝包含多個值的索引鍵。 本例的第一個索引鍵值是學生姓氏的第一個字母。 第二個索引鍵值是布林值,指定學生第一次的考試分數是否超過 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 迴圈,才能逐一查看巢狀群組的內部元素。
(將滑鼠游標暫留在反覆項目變數、outerGroupinnerGroupinnerGroupElement 上,以查看其實際型別。)

使用方法語法的對等查詢會顯示在下列程式碼中:

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}");
        }
    }
}

在分組作業上執行子查詢

本文說明兩種不同的建立查詢方式,將來源資料排序成群組,然後個別對每個群組執行子查詢。 每個範例中的基本技巧是使用名為 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}");
}

另請參閱