group 子句(C# 参考)
group
子句返回一个 IGrouping<TKey,TElement> 对象序列,这些对象包含零个或更多与该组的键值匹配的项。 例如,可以按照每个字符串中的第一个字母对字符串序列进行分组。 在这种情况下,第一个字母就是键,类型为 char,并且存储在每个 IGrouping<TKey,TElement> 对象的 Key
属性中。 编译器可推断键的类型。
可以用 group
子句结束查询表达式,如以下示例所示:
// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery1 =
from student in students
group student by student.Last[0];
如果要对每个组执行附加查询操作,可使用上下文关键字 into 指定一个临时标识符。 使用 into
时,必须继续编写该查询,并最终使用一个select
语句或另一个 group
子句结束该查询,如以下代码摘录所示:
// Group students by the first letter of their last name
// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery2 =
from student in students
group student by student.Last[0] into g
orderby g.Key
select g;
对于含有和不含 into
的 group
,本文中的“示例”部分提供有关其用法的更完整示例。
枚举查询分组的结果
由于 group
查询产生的 IGrouping<TKey,TElement> 对象实质上是一个由列表组成的列表,因此必须使用嵌套的 foreach 循环来访问每一组中的各个项。 外部循环用于循环访问组键,内部循环用于循环访问组本身包含的每个项。 组可能具有键,但没有元素。 下面的 foreach
循环执行上述代码示例中的查询:
// Iterate group items with a nested foreach. This IGrouping encapsulates
// a sequence of Student objects, and a Key of type char.
// For convenience, var can also be used in the foreach statement.
foreach (IGrouping<char, Student> studentGroup in studentQuery2)
{
Console.WriteLine(studentGroup.Key);
// Explicit type for student could also be used here.
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}", student.Last, student.First);
}
}
键类型
组键可以是任何类型,如字符串、内置数值类型、用户定义的命名类型或匿名类型。
按字符串分组
上述代码示例使用 char
。 可轻松改为指定字符串键,如完整的姓氏:
// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;
按布尔值分组
下面的示例演示使用布尔值作为键将结果划分成两个组。 请注意,该值由 group
子句中的子表达式生成。
class GroupSample1
{
// The element type of the data source.
public class Student
{
public required string First { get; init; }
public required string Last { get; init; }
public required int ID { get; init; }
public required List<int> Scores;
}
public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students =
[
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= [97, 72, 81, 60]},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= [75, 84, 91, 39]},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= [99, 89, 91, 95]},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= [72, 81, 65, 84]},
new Student {First="Debra", Last="Garcia", ID=115, Scores= [97, 89, 85, 82]}
];
return students;
}
static void Main()
{
// Obtain the data source.
List<Student> students = GetStudents();
// Group by true or false.
// Query variable is an IEnumerable<IGrouping<bool, Student>>
var booleanGroupQuery =
from student in students
group student by student.Scores.Average() >= 80; //pass or fail!
// Execute the query and access items in each group
foreach (var studentGroup in booleanGroupQuery)
{
Console.WriteLine(studentGroup.Key == true ? "High averages" : "Low averages");
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
}
}
}
/* Output:
Low averages
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
High averages
Mortensen, Sven:93.5
Garcia, Debra:88.25
*/
按数值范围分组
下一示例使用表达式创建表示百分比范围的数值组键。 请注意,该示例使用 let 作为方法调用结果的方便存储位置,因此无需在 group
子句中调用该方法两次。 若要详细了解如何在查询表达式中安全使用方法,请参阅在查询表达式中处理异常。
class GroupSample2
{
// The element type of the data source.
public class Student
{
public required string First { get; init; }
public required string Last { get; init; }
public required int ID { get; init; }
public required List<int> Scores;
}
public static List<Student> GetStudents()
{
// Use a collection initializer to create the data source. Note that each element
// in the list contains an inner sequence of scores.
List<Student> students =
[
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= [97, 72, 81, 60]},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= [75, 84, 91, 39]},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= [99, 89, 91, 95]},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= [72, 81, 65, 84]},
new Student {First="Debra", Last="Garcia", ID=115, Scores= [97, 89, 85, 82]}
];
return students;
}
// This method groups students into percentile ranges based on their
// grade average. The Average method returns a double, so to produce a whole
// number it is necessary to cast to int before dividing by 10.
static void Main()
{
// Obtain the data source.
List<Student> students = GetStudents();
// Write the query.
var studentQuery =
from student in students
let avg = (int)student.Scores.Average()
group student by (avg / 10) into g
orderby g.Key
select g;
// Execute the query.
foreach (var studentGroup in studentQuery)
{
int temp = studentGroup.Key * 10;
Console.WriteLine("Students with an average between {0} and {1}", temp, temp + 10);
foreach (var student in studentGroup)
{
Console.WriteLine(" {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
}
}
}
}
/* Output:
Students with an average between 70 and 80
Omelchenko, Svetlana:77.5
O'Donnell, Claire:72.25
Garcia, Cesar:75.5
Students with an average between 80 and 90
Garcia, Debra:88.25
Students with an average between 90 and 100
Mortensen, Sven:93.5
*/
按复合键分组
希望按照多个键对元素进行分组时,可使用复合键。 使用匿名类型或命名类型来存储键元素,创建复合键。 在下面的示例中,假定已经使用名为 surname
和 city
的两个成员声明了类 Person
。 group
子句会为每组姓氏和城市相同的人员创建一个单独的组。
group person by new {name = person.surname, city = person.city};
如果必须将查询变量传递给其他方法,请使用命名类型。 使用自动实现的键属性创建一个特殊类,然后重写 Equals 和 GetHashCode 方法。 还可以使用结构,在此情况下,并不严格要求替代这些方法。 有关详细信息,请参阅 如何使用自动实现的属性 实现轻型类,以及如何 查询目录树中的重复文件。 后文包含的代码示例演示了如何将复合键与命名类型结合使用。
示例 1
下面的示例演示在没有向组应用附加查询逻辑时,将源数据按顺序放入组中的标准模式。 这称为不带延续的分组。 字符串数组中的元素按照它们的首字母进行分组。 查询的结果是 IGrouping<TKey,TElement> 类型(包含一个 char
类型的公共 Key
属性)和一个 IEnumerable<T> 集合(在分组中包含每个项)。
group
子句的结果是由序列组成的序列。 因此,若要访问返回的每个组中的单个元素,请在循环访问组键的循环内使用嵌套的 foreach
循环,如以下示例所示。
class GroupExample1
{
static void Main()
{
// Create a data source.
string[] words = ["blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese"];
// Create the query.
var wordGroups =
from w in words
group w by w[0];
// Execute the query.
foreach (var wordGroup in wordGroups)
{
Console.WriteLine("Words that start with the letter '{0}':", wordGroup.Key);
foreach (var word in wordGroup)
{
Console.WriteLine(word);
}
}
}
}
/* Output:
Words that start with the letter 'b':
blueberry
banana
Words that start with the letter 'c':
chimpanzee
cheese
Words that start with the letter 'a':
abacus
apple
*/
示例 2
此示例演示在创建组之后,如何使用通过 into
实现的延续对这些组执行附加逻辑。 有关详细信息,请参阅 into。 下面的示例查询每个组,仅选择键值为元音的元素。
class GroupClauseExample2
{
static void Main()
{
// Create the data source.
string[] words2 = ["blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese", "elephant", "umbrella", "anteater"];
// Create the query.
var wordGroups2 =
from w in words2
group w by w[0] into grps
where (grps.Key == 'a' || grps.Key == 'e' || grps.Key == 'i'
|| grps.Key == 'o' || grps.Key == 'u')
select grps;
// Execute the query.
foreach (var wordGroup in wordGroups2)
{
Console.WriteLine("Groups that start with a vowel: {0}", wordGroup.Key);
foreach (var word in wordGroup)
{
Console.WriteLine(" {0}", word);
}
}
}
}
/* Output:
Groups that start with a vowel: a
abacus
apple
anteater
Groups that start with a vowel: e
elephant
Groups that start with a vowel: u
umbrella
*/
备注
在编译时,group
子句转换为对 GroupBy 方法的调用。
group
子句查询的语法不支持自定义相等比较器。 若要在查询中使用 IEqualityComparer,请显式使用 GroupBy 方法。