Avatar ·

Группировка по нескольким полям с помощью LINQ в C#

Всем привет! Я начинающий разработчик на C# и столкнулся с проблемой при использовании LINQ. У меня есть список объектов класса `Sale`, который содержит информацию о продажах: название магазина (`Store`), дату (`Date`) и сумму продажи (`Amount`). Мне нужно сгруппировать все продажи по магазину и по дате (то есть получить итоговую сумму для каждого магазина за каждый день). Вот мой класс: ```csharp public class Sale { public string Store { get; set; } public DateTime Date { get; set; } public decimal Amount { get; set; } } ``` И пример данных: ```csharp var sales = new List { new Sale { Store = "Магазин A", Date = new DateTime(2023, 1, 1), Amount = 100 }, new Sale { Store = "Магазин A", Date = new DateTime(2023, 1, 1), Amount = 150 }, new Sale { Store = "Магазин B", Date = new DateTime(2023, 1, 1), Amount = 200 }, new Sale { Store = "Магазин A", Date = new DateTime(2023, 1, 2), Amount = 120 }, new Sale { Store = "Магазин B", Date = new DateTime(2023, 1, 2), Amount = 180 }, }; ``` Я пробовал использовать `GroupBy` только по одному полю, например `sales.GroupBy(s => s.Store)`, это работает. Но как сделать группировку одновременно по `Store` и `Date`? Пытался писать `sales.GroupBy(s => s.Store, s => s.Date)` — это не компилируется. Подскажите, пожалуйста, правильный синтаксис. Нужно, чтобы на выходе я мог получить для каждой пары (Магазин + Дата) общую сумму (`Amount`). Заранее спасибо! <<>>

Группировка по нескольким полям с помощью анонимного типа

В LINQ для группировки по нескольким полям нужно использовать анонимный тип (или кортеж в современных версиях C#). Ключ группировки должен быть одним объектом, который содержит все поля, по которым вы хотите группировать. Анонимный тип создается с помощью ключевого слова new без указания имени класса.

Вот правильное решение для вашей задачи:

var groupedSales = sales
    .GroupBy(s => new { s.Store, s.Date })
    .Select(g => new 
    {
        Store = g.Key.Store,
        Date = g.Key.Date,
        TotalAmount = g.Sum(s => s.Amount)
    });

foreach (var item in groupedSales)
{
    Console.WriteLine($"{item.Store} | {item.Date.ToShortDateString()} | Сумма: {item.TotalAmount}");
}

Результат выполнения:

Магазин A | 01.01.2023 | Сумма: 250
Магазин B | 01.01.2023 | Сумма: 200
Магазин A | 02.01.2023 | Сумма: 120
Магазин B | 02.01.2023 | Сумма: 180

Как это работает

Разберем по шагам:

  • GroupBy(s => new { s.Store, s.Date }) — создает группы, где ключом является анонимный объект с двумя свойствами: Store и Date. Все записи с одинаковым магазином и датой попадают в одну группу.
  • Результат GroupBy — это коллекция групп (тип IGrouping<TKey, TElement>). Каждая группа содержит свойство Key (наш анонимный объект) и коллекцию элементов этой группы.
  • .Select(...) — проекция каждой группы в новый анонимный объект с нужными полями. Через g.Key.Store и g.Key.Date мы получаем значения ключа, а g.Sum(s => s.Amount) суммирует все Amount внутри группы.

Альтернатива с кортежами (C# 7.0+)

Если вы используете C# 7.0 или новее, можно применить кортежи (ValueTuple). Это делает код более читаемым, особенно когда полей много:

var groupedSales = sales
    .GroupBy(s => (s.Store, s.Date))
    .Select(g => new 
    {
        Store = g.Key.Store,
        Date = g.Key.Date,
        TotalAmount = g.Sum(s => s.Amount)
    });

Обратите внимание: в GroupBy теперь используется (s.Store, s.Date) — это синтаксис кортежа. В остальном логика идентична.

Синтаксис запросов (query syntax)

Тот же результат можно получить с помощью синтаксиса запросов LINQ (напоминает SQL):

var groupedSales = from s in sales
                   group s by new { s.Store, s.Date } into g
                   select new 
                   {
                       Store = g.Key.Store,
                       Date = g.Key.Date,
                       TotalAmount = g.Sum(s => s.Amount)
                   };

Этот вариант может быть привычнее для тех, кто знает SQL.

Важные моменты

  • Сравнение ключей: Анонимные типы и кортежи автоматически реализуют Equals и GetHashCode, поэтому группировка работает корректно. Для собственных классов-ключей нужно переопределять эти методы.
  • Производительность: Группировка по нескольким полям незначительно медленнее, чем по одному, но для большинства задач это незаметно. Если данных очень много (миллионы записей), рассмотрите индексацию в базе данных.
  • Порядок полей: В анонимном типе порядок свойств имеет значение. new { s.Store, s.Date } и new { s.Date, s.Store } — это разные ключи, хотя данные могут быть похожими. Будьте последовательны.

Теперь вы можете гибко группировать данные по любому количеству полей. Этот подход применяется не только в памяти (LINQ to Objects), но и в Entity Framework (LINQ to Entities) для генерации SQL-запросов с GROUP BY.

<<>>

Использование вложенных GroupBy

Иногда, особенно в учебных целях, можно применить вложенную группировку: сначала сгруппировать по одному полю, а затем внутри каждой группы — по другому. Это менее эффективно, но наглядно показывает структуру данных.

var nestedGrouping = sales
    .GroupBy(s => s.Store)
    .Select(storeGroup => new
    {
        Store = storeGroup.Key,
        DateGroups = storeGroup.GroupBy(s => s.Date)
                               .Select(dateGroup => new
                               {
                                   Date = dateGroup.Key,
                                   TotalAmount = dateGroup.Sum(s => s.Amount)
                               })
    });

foreach (var store in nestedGrouping)
{
    Console.WriteLine($"Магазин: {store.Store}");
    foreach (var date in store.DateGroups)
    {
        Console.WriteLine($"  {date.Date.ToShortDateString()}: {date.TotalAmount}");
    }
}

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

Недостатки вложенного GroupBy:

  • Больше кода и вложенных циклов.
  • Меньшая производительность из-за двойной группировки.
  • Сложнее использовать для дальнейшей обработки (например, для записи в таблицу "Магазин-Дата-Сумма").

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