Предложение group (Справочник по C#)

Предложение group возвращает последовательность объектов IGrouping, содержащих ноль или более элементов, соответствующих значению ключа группы. Например, можно сгруппировать последовательность строк в соответствии с первой буквой каждой из строк. В этом случае первая буква является ключом и имеет тип char. Она хранится в свойстве Key каждого из объектов IGrouping. Тип ключа определяется компилятором.

Завершить выражение запроса с предложением 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;

Более подробные примеры использования предложения group с ключевым словом into и без него см. в разделе "Пример".

Перечисление результатов запроса group

Поскольку возвращаемые запросом group объекты IGrouping представляют собой список списков, для доступа к каждому из элементов этих групп необходимо использовать вложенный цикл 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
 */

Группировка по составному ключу

Составные ключи используются в тех случаях, когда необходимо сгруппировать элементы по более чем одному ключу. Составной ключ создается с помощью анонимного типа или именованного типа для хранения элементов ключа. В следующем примере предполагается, что для класса Person были объявлены члены surname и city. Предложение group позволяет создать отдельную группу для каждого набора людей, имеющих одинаковые фамилии и города.

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

Если требуется передать переменную запроса другому методу, следует использовать именованные типы. Создайте специальный класс, используя для ключей автоматически реализуемые свойства, и переопределите методы Equals и GetHashCode. Можно также воспользоваться структурой, чтобы избежать необходимости переопределять эти методы. Дополнительные сведения см. в разделах Практическое руководство. Реализация облегченного класса с автоматически реализуемыми свойствами (Руководство по программированию в C#) и Практическое руководство. Запрос повторяющихся файлов в дереве каталогов (LINQ). В последнем разделе имеется пример кода, демонстрирующий использование составных ключей с именованным типом.

Пример

В следующем примере показан стандартный шаблон для разбиения данных на группы без применения к группам дополнительной логики запроса. Такой прием называется группировкой без продолжения. Элементы массива строк группируются в соответствии со своими первыми буквами. Результат выполнения запроса имеет тип IGrouping и содержит открытое свойство Key типа char и коллекцию IEnumerable с элементами групп.

Результатом выполнения предложения 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
     */

В этом примере показано выполнение дополнительных операций над группами после их создания. Для этого используются ключевые слова continuation и 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``2.

См. также

Задачи

Практическое руководство. Создание вложенной группы (Руководство по программированию на C#)

Практическое руководство. Группировка результатов запросов (Руководство по программированию на C#)

Практическое руководство. Вложенный запрос в операции группирования (Руководство по программированию на C#)

Ссылки

IGrouping

GroupBy``2

ThenBy``2

ThenByDescending``2

Основные понятия

Выражения запросов LINQ (Руководство по программированию на C#)

Другие ресурсы

Ключевые слова запроса (Справочник по C#)