TOC

This article is currently in the process of being translated into Russian (~99% done).

LINQ:

Grouping data: the GroupBy() Method

До сих пор мы работали в основном со списками данных. Мы отсортировали его, ограничили и сформировали в новые объекты, но по-прежнему не хватает одной важной операции: группировки данных. Когда вы группируете данные, вы берете список чего-либо, а затем делите его на несколько групп на основе одного или нескольких свойств. Просто представьте, что у нас есть такой источник данных, как этот:

var users = new List<User>()
{
    new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
    new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
    new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
    new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
    new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
};

Плоский список пользовательских объектов, но было бы интересно сгруппировать этих пользователей, например, по их родной стране или возрасту. С LINQ это очень просто, хотя использование метода GroupBy() вначале может немного сбить с толку. Давайте посмотрим, как это работает:

using System;    
using System.Collections.Generic;    
using System.Linq;    

namespace LinqGroup    
{    
    class Program    
    {    
static void Main(string[] args)    
{    
    var users = new List<User>()    
    {    
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },    
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },    
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },    
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },    
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },    
    };    
    var usersGroupedByCountry = users.GroupBy(user => user.HomeCountry);    
    foreach(var group in usersGroupedByCountry)    
    {    
Console.WriteLine("Users from " + group.Key + ":");    
foreach(var user in group)    
Console.WriteLine("* " + user.Name);
    }    
}    

public class User    
{    
    public string Name { get; set; }    

    public int Age { get; set; }    

    public string HomeCountry { get; set; }    
}    
    }    
}

Результирующий выход будет выглядеть примерно так:

Users from USA:
* John Doe
* Jane Doe
* James Doe
Users from Germany:
* Joe Doe
* Jenna Doe

Пример может показаться немного длинным, но, как вы скоро поймете, большая его часть - это просто подготовка источника данных. Помните, что все данные с таким же успехом могут быть получены из XML-документа или базы данных - просто это проще продемонстрировать с помощью объектного источника данных, который вы можете использовать как есть.

Интересная часть заключается в том, что мы создаем переменную usersGroupedByCountry. Мы делаем это, вызывая метод GroupBy() в нашем источнике данных, предоставляя параметр, по которому мы хотим сгруппировать данные. В этом случае я хочу, чтобы пользователи были сгруппированы по их родной стране, поэтому это свойство я предоставляю методу GroupBy(). Результатом является объект с ключевым свойством, который содержит значение свойства, по которому мы сгруппировали (в данном случае HomeCountry), а также все объекты, принадлежащие группе. Мы используем это в следующих строках для перебора групп, которые мы только что создали, и для каждой группы мы печатаем Ключ (HomeCountry), а затем повторяем и печатаем все пользовательские объекты из группы.

Пользовательские групповые ключи

Как вы можете видеть, группировка по существующему свойству проста, но, как вы, возможно, уже знаете, методы LINQ очень гибкие. Так же просто создать свои собственные, настраиваемые группы, основанные на том, что вам нравится - примером этого может быть следующее, где мы создаем группы на основе первых двух букв имени пользователя.:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup
{
    class Program
    {
static void Main(string[] args)
{
    var users = new List<User>()
    {
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
    };
    var usersGroupedByFirstLetters = users.GroupBy(user => user.Name.Substring(0, 2));
    foreach(var group in usersGroupedByFirstLetters)
    {
Console.WriteLine("Users starting with " + group.Key + ":");
foreach(var user in group)
    Console.WriteLine("* " + user.Name);
    }
}

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string HomeCountry { get; set; }
}
    }
}

Мы просто вызываем метод Substring() для имени, чтобы получить две первые буквы, а затем LINQ создает группы пользователей на его основе. Результат будет выглядеть примерно так:

Users starting with Jo:
* John Doe
* Joe Doe
Users starting with Ja:
* Jane Doe
* James Doe
Users starting with Je:
* Jenna Doe

Итак, как вы можете видеть, мы можем свободно вызывать метод внутри метода GroupBy() - на самом деле, мы можем делать там практически все, что захотим, при условии, что мы возвращаем что-то, что LINQ может использовать для группировки элементов. Мы даже можем создать метод, который возвращает новую информацию об элементе, а затем использовать его для создания группы, как мы делаем в следующем примере:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup
{
    class Program
    {
static void Main(string[] args)
{
    var users = new List<User>()
    {
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
    };
    var usersGroupedByAgeGroup = users.GroupBy(user => user.GetAgeGroup());
    foreach(var group in usersGroupedByAgeGroup)
    {
Console.WriteLine(group.Key + ":");
foreach(var user in group)
    Console.WriteLine("* " + user.Name + " [" + user.Age + " years]");
    }
}

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string HomeCountry { get; set; }

    public string GetAgeGroup()
    {
if (this.Age < 13)
    return "Children";
if (this.Age < 20)
    return "Teenagers";
return "Adults";
    }
}
    }
}

Обратите внимание, как я реализовал метод GetAgeGroup() в классе User. Он возвращает строку, которая определяет возрастную группу пользователя, и мы просто вызываем ее в методе GroupBy(), чтобы использовать ее в качестве группового ключа. Результат будет выглядеть следующим образом:

Adults:
* John Doe [42 years]
* Jane Doe [38 years]
Teenagers:
* Joe Doe [19 years]
* Jenna Doe [19 years]
Children:
* James Doe [8 years]

Я решил реализовать метод GetAgeGroup() в классе User, потому что он может быть полезен в других местах, но иногда вам просто нужна быстрая логика для создания групп, а не для повторного использования в другом месте. В таких ситуациях вы можете предоставить логику непосредственно методу GroupBy() в виде лямбда-выражения, например:

var usersGroupedByAgeGroup = users.GroupBy(user =>
    {
if (user.Age < 13)
    return "Children";
if (user.Age < 20)
    return "Teenagers";
return "Adults";
    });

Результат, конечно, тот же самый!

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

До сих пор ключи наших групп были просто одним значением, например свойством или результатом вызова метода. Однако вы можете свободно создавать свои собственные ключи, которые содержат несколько значений - они называются составными ключами. Примером использования было бы, если бы мы хотели сгруппировать наших пользователей на основе как их родной страны, так и их возраста, вот так:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup2
{
    class Program
    {
static void Main(string[] args)
{
    var users = new List<User>()
    {
new User { Name = "John Doe", Age = 42, HomeCountry = "USA" },
new User { Name = "Jane Doe", Age = 38, HomeCountry = "USA" },
new User { Name = "Joe Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "Jenna Doe", Age = 19, HomeCountry = "Germany" },
new User { Name = "James Doe", Age = 8, HomeCountry = "USA" },
    };

    var usersGroupedByCountryAndAge = users.GroupBy(user => new { user.HomeCountry, user.Age });
    foreach(var group in usersGroupedByCountryAndAge)
    {
Console.WriteLine("Users from " + group.Key.HomeCountry + " at the age of " + group.Key.Age + ":");
foreach (var user in group)
    Console.WriteLine("* " + user.Name + " [" + user.Age + " years]");
    }
}

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string HomeCountry { get; set; }

}
    }
}

Обратите внимание на синтаксис, который мы используем в методе GroupBy() - вместо предоставления отдельного свойства мы создаем новый анонимный объект, который содержит свойства HomeCountry и Age. LINQ теперь создаст группы на основе этих двух свойств и присоединит анонимный объект к ключевому свойству группы. Как вы можете видеть, мы можем свободно использовать оба свойства при переборе групп. Результат будет выглядеть примерно так:

Users from USA at the age of 42:
* John Doe [42 years]
Users from USA at the age of 38:
* Jane Doe [38 years]
Users from Germany at the age of 19:
* Joe Doe [19 years]
* Jenna Doe [19 years]
Users from USA at the age of 8:
* James Doe [8 years]

Как всегда, в этой статье мы использовали синтаксис метода LINQ, но позвольте мне предоставить вам сравнительный пример того, как это можно было бы сделать с помощью синтаксиса запроса LINQ:

// Method syntax
var usersGroupedByCountryAndAge = users.GroupBy(user => new { user.HomeCountry, user.Age });
// Query syntax
var usersGroupedByCountryAndAgeQ = from user in users group user by new { user.HomeCountry, user.Age } into userGroup select userGroup;

Summary

Как вы, вероятно, можете видеть из примеров в этой статье, метод GroupBy() LINQ чрезвычайно мощный. Это действительно позволяет вам использовать ваши данные по-новому, с очень небольшим количеством кода. Раньше это было бы либо очень громоздко, либо требовало реляционной базы данных, но с LINQ вы можете использовать любой источник данных, который вам нравится, и при этом получать ту же простую в использовании функциональность.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!