分組資料 (C#)
分組指的是將資料放在群組中,好讓每一個群組中的項目共用共同的屬性。 下圖顯示一系列字元的分組結果。 每個群組的索引鍵是字元。
重要
這些範例會使用 System.Collections.Generic.IEnumerable<T> 資料來源。 根據 System.Linq.IQueryProvider 的資料來源會使用 System.Linq.IQueryable<T> 資料來源和運算式樹狀架構。 運算式樹狀架構在允許的 C# 語法方面有限制。 此外,每個 IQueryProvider
資料來源 (例如 EF Core) 可能會施加更多限制。 檢查資料來源的文件。
分組資料元素的標準查詢運算子方法詳列於以下表格。
方法名稱 | 描述 | 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);
}
}
注意
本文中的下列範例會使用此區域的通用數據源。
每個 Student
都有一個等級、一個主要部門和一系列分數。 Teacher
也有一個 City
屬性,可識別教師持有課程的校園。 Department
具有名稱,以及擔任部門負責人 Teacher
的參考。
您可以在來源存放庫中找到範例數據集。
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; }
}
將查詢結果分組
分組是 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
迴圈,才能逐一查看巢狀群組的內部元素。
(將滑鼠游標暫留在反覆項目變數、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}");
}
}
}
在分組作業上執行子查詢
本文說明兩種不同的建立查詢方式,將來源資料排序成群組,然後個別對每個群組執行子查詢。 每個範例中的基本技巧是使用名為 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}");
}