group 子句(C# 参考)group clause (C# Reference)

group 子句返回一个 IGrouping<TKey,TElement> 对象序列,这些对象包含零个或更多与该组的键值匹配的项。The group clause returns a sequence of IGrouping<TKey,TElement> objects that contain zero or more items that match the key value for the group. 例如,可以按照每个字符串中的第一个字母对字符串序列进行分组。For example, you can group a sequence of strings according to the first letter in each string. 在这种情况下,第一个字母就是键,类型为 char,并且存储在每个 IGrouping<TKey,TElement> 对象的 Key 属性中。In this case, the first letter is the key and has a type char, and is stored in the Key property of each IGrouping<TKey,TElement> object. 编译器可推断键的类型。The compiler infers the type of the key.

可以用 group 子句结束查询表达式,如以下示例所示:You can end a query expression with a group clause, as shown in the following example:

// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery1 =
    from student in students
    group student by student.Last[0];

如果要对每个组执行附加查询操作,可使用上下文关键字 into 指定一个临时标识符。If you want to perform additional query operations on each group, you can specify a temporary identifier by using the into contextual keyword. 使用 into 时,必须继续编写该查询,并最终使用一个select 语句或另一个 group 子句结束该查询,如以下代码摘录所示:When you use into, you must continue with the query, and eventually end it with either a select statement or another group clause, as shown in the following excerpt:

// 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,本文中的“示例”部分提供有关其用法的更完整示例。More complete examples of the use of group with and without into are provided in the Example section of this article.

枚举查询分组的结果Enumerating the results of a group query

由于 group 查询产生的 IGrouping<TKey,TElement> 对象实质上是一个由列表组成的列表,因此必须使用嵌套的 foreach 循环来访问每一组中的各个项。Because the IGrouping<TKey,TElement> objects produced by a group query are essentially a list of lists, you must use a nested foreach loop to access the items in each group. 外部循环用于循环访问组键,内部循环用于循环访问组本身包含的每个项。The outer loop iterates over the group keys, and the inner loop iterates over each item in the group itself. 组可能具有键,但没有元素。A group may have a key but no elements. 下面的 foreach 循环执行上述代码示例中的查询:The following is the foreach loop that executes the query in the previous code examples:

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

键类型Key types

组键可以是任何类型,如字符串、内置数值类型、用户定义的命名类型或匿名类型。Group keys can be any type, such as a string, a built-in numeric type, or a user-defined named type or anonymous type.

按字符串分组Grouping by string

上述代码示例使用 charThe previous code examples used a char. 可轻松改为指定字符串键,如完整的姓氏:A string key could easily have been specified instead, for example the complete last name:

// 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;

按布尔值分组Grouping by bool

下面的示例演示使用布尔值作为键将结果划分成两个组。The following example shows the use of a bool value for a key to divide the results into two groups. 请注意,该值由 group 子句中的子表达式生成。Note that the value is produced by a sub-expression in the group clause.

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
*/

按数值范围分组Grouping by numeric range

下一示例使用表达式创建表示百分比范围的数值组键。The next example uses an expression to create numeric group keys that represent a percentile range. 请注意,该示例使用 let 作为方法调用结果的方便存储位置,因此无需在 group 子句中调用该方法两次。Note the use of let as a convenient location to store a method call result, so that you don't have to call the method two times in the group clause. 若要详细了解如何在查询表达式中安全使用方法,请参阅在查询表达式中处理异常For more information about how to safely use methods in query expressions, see Handle exceptions in query expressions.

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 / 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
 */

按复合键分组Grouping by composite keys

希望按照多个键对元素进行分组时,可使用复合键。Use a composite key when you want to group elements according to more than one key. 使用匿名类型或命名类型来存储键元素,创建复合键。You create a composite key by using an anonymous type or a named type to hold the key element. 在下面的示例中,假定已经使用名为 surnamecity 的两个成员声明了类 PersonIn the following example, assume that a class Person has been declared with members named surname and city. group 子句会为每组姓氏和城市相同的人员创建一个单独的组。The group clause causes a separate group to be created for each set of persons with the same last name and the same city.

group person by new {name = person.surname, city = person.city};

如果必须将查询变量传递给其他方法,请使用命名类型。Use a named type if you must pass the query variable to another method. 使用键的自动实现的属性创建一个特殊类,然后替代 EqualsGetHashCode 方法。Create a special class using auto-implemented properties for the keys, and then override the Equals and GetHashCode methods. 还可以使用结构,在此情况下,并不严格要求替代这些方法。You can also use a struct, in which case you do not strictly have to override those methods. 有关详细信息,请参阅如何使用自动实现的属性实现轻量类如何在目录树中查询重复文件For more information see How to implement a lightweight class with auto-implemented properties and How to query for duplicate files in a directory tree. 后文包含的代码示例演示了如何将复合键与命名类型结合使用。The latter article has a code example that demonstrates how to use a composite key with a named type.

示例Example

下面的示例演示在没有向组应用附加查询逻辑时,将源数据按顺序放入组中的标准模式。The following example shows the standard pattern for ordering source data into groups when no additional query logic is applied to the groups. 这称为不带延续的分组。This is called a grouping without a continuation. 字符串数组中的元素按照它们的首字母进行分组。The elements in an array of strings are grouped according to their first letter. 查询的结果是 IGrouping<TKey,TElement> 类型(包含一个 char 类型的公共 Key 属性)和一个 IEnumerable<T> 集合(在分组中包含每个项)。The result of the query is an IGrouping<TKey,TElement> type that contains a public Key property of type char and an IEnumerable<T> collection that contains each item in the grouping.

group 子句的结果是由序列组成的序列。The result of a group clause is a sequence of sequences. 因此,若要访问返回的每个组中的单个元素,请在循环访问组键的循环内使用嵌套的 foreach 循环,如以下示例所示。Therefore, to access the individual elements within each returned group, use a nested foreach loop inside the loop that iterates the group keys, as shown in the following example.

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
     */

示例Example

此示例演示在创建组之后,如何使用通过 into 实现的延续 对这些组执行附加逻辑。This example shows how to perform additional logic on the groups after you have created them, by using a continuation with into. 有关详细信息,请参阅 intoFor more information, see into. 下面的示例查询每个组,仅选择键值为元音的元素。The following example queries each group to select only those whose key value is a vowel.

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
*/    

备注Remarks

在编译时,group 子句转换为对 GroupBy 方法的调用。At compile time, group clauses are translated into calls to the GroupBy method.

请参阅See also