1. Вступ
Коли працюєте з колекціями, часто потрібно не просто пройтися кожним елементом і щось зробити, а отримати короткий «підсумок» для всього набору даних. Наприклад:
- Порахувати, скільки студентів отримали пʼятірку.
- Дізнатися, скільки загалом учнів у школі.
- Підрахувати, яку суму балів набрали студенти на іспиті.
- Знайти максимальну та мінімальну оцінку.
- Обчислити середній бал у класі.
Звісно, усі ці завдання можна розв’язати звичайним циклом із лічильником. Погодьтеся: мало хто радіє ідеї писати два десятки рядків коду для такої елементарної речі.
LINQ надає набір агрегатних функцій — готових методів, які беруть колекцію, проходяться всіма її елементами й повертають відповідь: суму, середнє, максимум, мінімум, а іноді й щось складніше.
Ось коротка «таблиця агрегатів»:
| Метод | Що робить | Повертає |
|---|---|---|
|
Рахує кількість елементів | |
|
Рахує суму числових значень | Залежить від типу елементів (, , ...) |
|
Обчислює середнє арифметичне | або інший числовий тип |
|
Шукає максимальний елемент | Елемент колекції |
|
Шукає мінімальний елемент | Елемент колекції |
Усі ці методи — extension-методи для колекцій, які реалізують IEnumerable<T>. Детальніше — офіційна документація з агрегатних методів LINQ.
2. Готуємо дані для практики
Продовжимо працювати із простим застосунком для обліку студентів. Припустімо, маємо такий клас:
// Модель студента
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // Оцінка за пʼятибальною шкалою
public string Email { get; set; }
}
І список:
// Базовий список студентів
List<Student> students = new List<Student>
{
new Student { Name = "Іван", Grade = 5, Email = "ivan@example.com" },
new Student { Name = "Ольга", Grade = 4, Email = "olga@example.com" },
new Student { Name = "Артем", Grade = 3, Email = "artem@example.com" },
new Student { Name = "Дар’я", Grade = 5, Email = "darya@example.com" },
new Student { Name = "Петро", Grade = 2, Email = "petr@example.com" }
};
3. Підрахунок елементів: Count()
Найпростіша статистика
Припустімо, ви хочете дізнатися, скільки загалом у вас студентів:
int totalStudents = students.Count(); // 5
Console.WriteLine($"Усього студентів: {totalStudents}");
LINQ‑магія: усе просто! Метод Count() повертає кількість елементів у колекції.
Підрахунок з умовою
А скільки відмінників у класі?
int excellentStudents = students.Count(s => s.Grade == 5);
Console.WriteLine($"Відмінників: {excellentStudents}");
Тут ми передаємо до Count лямбду — враховуються лише ті, хто відповідає умові (s.Grade == 5). Усередині LINQ це фактично те саме, що Where(...).Count(), але коротше й трохи ефективніше.
Що, якщо колекція порожня?
Якщо колекція порожня, Count коректно поверне 0 — жодної помилки не буде.
4. Сумування значень: Sum()
Отримуємо суму всіх оцінок
Уявімо, що ви хочете дізнатися загальну суму балів, які набрав клас:
int sumOfGrades = students.Sum(s => s.Grade); // 5+4+3+5+2 = 19
Console.WriteLine($"Сума всіх оцінок: {sumOfGrades}");
Sum приймає селектор (лямбда-вираз), який повертає значення для кожного елемента.
Сумування для колекції чисел
Якщо у вас просто є список чисел, селектор не потрібен:
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // 15
Поширені пастки та нюанси
Якщо колекція порожня, Sum() для числових типів поверне 0. А якщо колекція складається з nullable-типів (int?, double?), то Sum також працює коректно: ігнорує значення null.
5. Середнє арифметичне: Average()
Рахуємо середній бал у класі
Класика: який середній бал у студентів?
double averageGrade = students.Average(s => s.Grade); // (5+4+3+5+2)/5 = 3.8
Console.WriteLine($"Середній бал: {averageGrade:F2}");
Average — дуже корисний інструмент для статистики. Зверніть увагу: метод завжди повертає double, навіть якщо початкові значення були int. Це дає змогу уникнути обрізання дробової частини.
Якщо немає жодного елемента
Якщо початкова колекція порожня, виклик Average() спричинить виняток InvalidOperationException. Це дуже поширена пастка: якщо ви не впевнені, що в колекції щось є, перевірте це заздалегідь!
if (students.Any())
Console.WriteLine(students.Average(s => s.Grade));
else
Console.WriteLine("Немає даних для обчислення середнього!");
Середнє значення для числового масиву
double avg = numbers.Average();
6. Максимум і мінімум: Max() і Min()
Хто у класі — зірка, а хто в аутсайдерах
Хочете дізнатися, хто тягне клас угору своїми оцінками, а хто, навпаки, викликає занепокоєння у викладача? Легко:
int maxGrade = students.Max(s => s.Grade); // 5
int minGrade = students.Min(s => s.Grade); // 2
Console.WriteLine($"Максимальна оцінка: {maxGrade}");
Console.WriteLine($"Мінімальна оцінка: {minGrade}");
А хто саме цей герой?
Іноді важлива не просто цифра, а імʼя того, хто її здобув. Отримуємо об’єкт студента з найвищою оцінкою:
// Беремо першого відмінника після сортування за спаданням
var bestStudent = students.OrderByDescending(s => s.Grade).First();
Console.WriteLine($"Найкращий студент: {bestStudent.Name} ({bestStudent.Grade})");
У новіших версіях .NET є й MaxBy, який дозволяє зробити це елегантніше. Починаючи з .NET 6, він уже доступний, а в .NET 9 додали ще зручностей (див. MaxBy на Microsoft Docs). Але навіть без MaxBy можна цілком обійтися сортуванням і .First().
Пастки та відмінності
Якщо колекція порожня, виклик Max() і Min() також викине InvalidOperationException. Тому варто бути готовими до цього (особливо якщо ви не перевірили колекцію заздалегідь):
if (students.Any())
Console.WriteLine($"Максимальна оцінка: {students.Max(s => s.Grade)}");
else
Console.WriteLine("Немає студентів для пошуку максимуму.");
7. Приклади «ланцюжків» агрегатних методів
Часто агрегатні методи використовуються разом із фільтрацією та проєкцією:
// Середній бал серед відмінників
double avgExcellent = students
.Where(s => s.Grade == 5)
.Average(s => s.Grade); // завжди 5, але приклад показовий
// Сума балів серед студентів з оцінкою не нижчою за 4
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// Кількість унікальних поштових адрес (про всяк випадок)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
Саме тут LINQ розкривається на повну: просте комбінування операцій дає змогу писати виразний і читабельний код, який буде зрозумілий навіть вашому котові (ну, якщо кіт — Junior C# Developer).
8. Порівняння з «ручним» підходом: навіщо використовувати агрегати?
Для ясності порівняймо LINQ зі звичайними циклами на прикладі обчислення середнього бала:
Звичайний підхід:
int sum = 0;
int count = 0;
foreach (var s in students)
{
sum += s.Grade;
count++;
}
double average = (count != 0) ? (double)sum / count : 0;
LINQ:
double average = students.Average(s => s.Grade);
Навіть обробити порожню колекцію — простіше!
9. Корисні нюанси
Коротко про продуктивність та особливості реалізації
LINQ‑агрегати, на відміну від більшості операцій LINQ, виконуються одразу! Тобто коли ви викликаєте Sum(), Count(), Average(), Max(), Min(), прохід усією колекцією відбувається саме в цей момент. Ці методи повертають не колекції, а один підсумковий результат.
Важливо: якщо ви робите щось ресурсомістке перед агрегатом — наприклад, складну фільтрацію чи перетворення — це виконається лише один раз, у момент виклику агрегату.
Чи підтримують агрегатні методи query-синтаксис?
Початківці часто запитують: «Чи можна все це писати у query‑синтаксисі?» Коротка відповідь: сам query‑синтаксис не містить ключових слів для агрегатів, але ви завжди можете поєднати його з методним синтаксисом:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
Усередині дужок — звичайний query‑синтаксис, а потім викликаємо метод‑агрегат. Так роблять частіше, ніж здається!
10. Поширені помилки при використанні LINQ
Помилка №1: спроба застосувати Average(), Max() або Min() до порожньої колекції.
Якщо колекція порожня, Sum() і Count() спокійно повернуть 0, але Average(), Max() і Min() викинуть виняток. Перед викликом цих методів переконайтеся, що колекція містить хоча б один елемент.
Помилка №2: передача лямбди з неправильною сигнатурою.
Наприклад, якщо ви передали рядок замість числа в агрегатну функцію (Sum, Max та ін.), отримаєте помилку компіляції. Особливо легко помилитися під час використання анонімних методів.
Помилка №3: неефективна перевірка кількості елементів.
Виклик Where(...).Count() спершу фільтрує, а потім рахує елементи. Натомість використовуйте Count(predicate) — він одразу рахує кількість відповідних елементів і працює швидше.
Помилка №4: ігнорування особливостей nullable-типів під час агрегації.
Якщо ви рахуєте суму для int?, Sum() проігнорує значення null. Це коректна поведінка, але іноді вона може дати неочікуваний результат, якщо ви не враховуєте цього заздалегідь.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ