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;

本主题中的“示例”部分中提供了使用含有和不含 intogroup 的更完整示例。

枚举组查询的结果

由于 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 string First { get; set; }
        public string Last { get; set; }
        public int ID { get; set; }
        public 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 List<Student>
        {
           new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81, 60}},
           new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}},
           new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}},
           new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
           new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {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());
            }
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* 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 子句中调用该方法两次。另请注意,在 group 子句中,为了避免发生“被零除”异常,代码进行了相应检查以确保学生的平均成绩不为零。有关如何在查询表达式中安全使用方法的更多信息,请参见如何:在查询表达式中处理异常(C# 编程指南)

class GroupSample2
{
    // The element type of the data source.
    public class Student
    {
        public string First { get; set; }
        public string Last { get; set; }
        public int ID { get; set; }
        public 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 List<Student>
        {
           new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81, 60}},
           new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}},
           new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}},
           new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}},
           new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {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 == 0 ? 0 : 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());
            }
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* 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};

如果必须将查询变量传递给其他方法,请使用命名类型。使用自动实现的属性作为键来创建一个特殊类,然后重写 EqualsGetHashCode 方法。还可以使用结构;在此情况下,并不绝对需要重写这些方法。有关更多信息,请参见如何:使用自动实现的属性实现轻量类(C# 编程指南)How to: Query for Duplicate Files in a Directory Tree (LINQ)。后一个主题包含一个代码示例,该示例演示如何将复合键与命名类型结合使用。

下面的示例演示在没有向组应用附加查询逻辑时将源数据排序放入不同组中的标准模式。这称为不带延续的分组。字符串数组中的元素按照它们的第一个字母进行分组。查询结果是一个 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);
            }
        }

        // Keep the console window open in debug mode
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }        
}
/* 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
     */

此示例演示在创建组之后,如何使用通过 into 实现的延续对这些组执行附加逻辑。有关更多信息,请参见 into(C# 参考)。下面的示例查询每个组以仅选择那些键值为元音的元素。

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

        // Keep the console window open in debug mode
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* 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<TSource, TKey> 方法的调用。

请参阅

IGrouping<TKey, TElement>

GroupBy<TSource, TKey>

ThenBy<TSource, TKey>

ThenByDescending<TSource, TKey>

查询关键字(C# 参考)

LINQ 查询表达式(C# 编程指南)

如何:创建嵌套组(C# 编程指南)

如何:对查询结果进行分组(C# 编程指南)

如何:对分组操作执行子查询(C# 编程指南)