1. Вступ
У реальних проєктах майже кожен другий розробник витрачає добру частину часу на роботу з колекціями: фільтрує, рахує, шукає, сортує, кладе, дістає — коротко кажучи, поводиться з ними майже як із холодильником перед сном. Особливо часто доводиться витягувати, обробляти та агрегувати дані — чи то списки користувачів, товари в каталозі, рядки тексту або будь-які інші масиви.
Майже всі сучасні колекції у .NET підтримують функціональні методи — такі як Where, Select, Find, Any, All та інші. Їхня сила — в універсальності та лаконічності: ви просто передаєте «шматочок логіки» у вигляді лямбда-виразу, і колекція оживає, ніби ви завели новий мотор.
LINQ (Language Integrated Query) — це не просто синтаксичний цукор, а ціла міні-мова всередині C#, що дозволяє писати запити до даних так, ніби ви користуєтеся SQL або Excel. Тільки краще: прямо в коді, з автодоповненням, типами й налагоджувачем.
Але вся ця магія працює завдяки делегатам — а щоразу писати окремий метод заради фільтрації масиву доволі втомливо. І тут лямбда-вирази заходять як міні-функції «на місці», перетворюючи громіздкий код на елегантний і виразний.
2. Лямбда-вирази у стандартних методах колекцій
Лямбда-вирази особливо добре виявляють себе у стандартних методах колекцій, побудованих на делегатах, таких як Find, Exists, ForEach та багато інших.
Приклад: Пошук за умовою
Припустімо, у вас є список товарів:
using System;
using System.Collections.Generic;
// Наш клас товару
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
var products = new List<Product>
{
new Product { Name = "Кава", Price = 100 },
new Product { Name = "Чай", Price = 70 },
new Product { Name = "Молоко", Price = 80 }
};
// Знайдемо перший дорогий продукт (>90)
Product expensive = products.Find(p => p.Price > 90); // Використовуємо лямбду!
Console.WriteLine(expensive?.Name); // => Кава
Без лямбди довелося б писати окремий метод або анонімну функцію старого стилю. А так — один рядок, і код читається, наче англійською: «Знайди продукт, де ціна більша за 90».
Приклад: Перевірка наявності товару
bool hasCheap = products.Exists(p => p.Price < 75);
Console.WriteLine(hasCheap); // => True (бо "Чай" дешевший за 75)
Приклад: Обробка всіх елементів (ForEach)
Іноді потрібно щось зробити з кожним елементом:
products.ForEach(p => Console.WriteLine($"{p.Name}: {p.Price} євро"));
Про аналогії
Якщо коротко: лямбда-вирази в колекціях — це як кнопка «зробити красиво» у фоторедакторі. Натиснули — і готово!
3. Лямбда-вирази і LINQ: магія для колекцій
LINQ — це не лише зручно, а й глибоке занурення у функціональний стиль програмування. Більшість LINQ-методів очікує на вхід делегати — а отже, їхній ідеальний напарник — лямбда-вирази.
Фільтрація з Where
Нехай у нас знову є список продуктів. Відіберемо лише «дешеві» товари:
using System.Linq;
var cheapProducts = products.Where(p => p.Price < 90);
foreach (var p in cheapProducts)
Console.WriteLine(p.Name); // Чай, Молоко
Отримали нову послідовність, не написавши жодного циклу вручну. Where приймає лямбду-предикат (функцію, що повертає true/false) і застосовує її до кожного елемента.
Сортування з OrderBy
Якщо любите порядок — ось приклад:
var sorted = products.OrderBy(p => p.Price);
foreach (var p in sorted)
Console.WriteLine($"{p.Name}: {p.Price}");
// Чай: 70
// Молоко: 80
// Кава: 100
Мапінг (Select) — проєкція даних
Часом нам потрібна не вся сутність, а лише її частина, наприклад, список назв товарів:
var names = products.Select(p => p.Name);
foreach (var name in names)
Console.WriteLine(name); // Кава, Чай, Молоко
Ланцюжки LINQ
LINQ зручний тим, що можна «ланцюжити» виклики один за одним:
var namesOfCheap = products
.Where(p => p.Price < 90)
.OrderBy(p => p.Name)
.Select(p => p.Name.ToUpper());
foreach (var name in namesOfCheap)
Console.WriteLine(name); // МОЛОКО, ЧАЙ
Схоже на конвеєр: кожен метод — новий етап обробки.
Питання: Чим лямбда-вирази кращі за звичайні методи для LINQ?
По-перше, лямбди можна писати прямо там, де вони потрібні. По-друге, лямбда-вирази короткі та читабельні. По-третє, це стандарт сучасного C# — так пишуть усі; ті, хто не пише, зазвичай співбесіди не проходять.
4. Практичний приклад
Протягом курсу ми писали навчальний застосунок для роботи з невеликим каталогом товарів, користувачів або замовлень. Додамо до нього сучасні методи обробки колекцій.
Пошук користувача за імʼям
public class User
{
public string Username { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User{ Username = "Alice", Age = 21 },
new User{ Username = "Bob", Age = 26 },
new User{ Username = "Charlie", Age = 32 }
};
// Пошук користувача за ім'ям
User found = users.FirstOrDefault(u => u.Username == "Bob");
Console.WriteLine(found?.Age); // 26
Фільтрація за віком
var adults = users.Where(u => u.Age >= 18);
foreach (var u in adults)
Console.WriteLine(u.Username); // Alice, Bob, Charlie
Підрахунок кількості користувачів
int count = users.Count(u => u.Age > 25);
Console.WriteLine(count); // 2 (Bob і Charlie)
Перевірка всіх користувачів на повноліття
bool allAdults = users.All(u => u.Age >= 18);
Console.WriteLine(allAdults); // True
Чи є хоч один неповнолітній?
bool hasMinor = users.Any(u => u.Age < 18);
Console.WriteLine(hasMinor); // False
5. LINQ: як це все працює зсередини
Коли ви пишете, наприклад, Where(u => u.Age > 20), це, по суті, те саме, що створити цикл, який перебирає всі елементи та перевіряє для кожного умову. Тільки LINQ робить це непомітно й красиво, загортаючи ваш предикат у делегат.
Без лямбда-виразів довелося б писати щось таке:
public static bool AgeMoreThan20(User u) => u.Age > 20;
var adultUsers = users.Where(AgeMoreThan20);
Або взагалі анонімні методи у старому стилі:
var adultUsers = users.Where(delegate(User u) { return u.Age > 20; });
Це все громіздко й нудно. З лямбдою — вишукано та сучасно.
6. Делегати і стандартні типи: Func, Action, Predicate
Не лише LINQ широко використовує лямбда-вирази. Багато методів стандартних колекцій приймають спеціалізовані делегати, наприклад:
- Predicate<T> — для методів Find, Exists, RemoveAll
- Func<T, TResult> — для LINQ-методів, проєкцій, обчислень
- Action<T> — для методів, які щось роблять з елементом, але нічого не повертають (ForEach)
Ось як це виглядає на практиці:
// Predicate<T>
users.RemoveAll(u => u.Age < 30); // Видалили всіх молодших за 30
// Func<T, TResult>
var names = users.Select(u => u.Username);
// Action<T>
users.ForEach(u => Console.WriteLine(u.Username));
7. Шпаргалка по методах колекцій з лямбда-виразами
| Метод | Що робить | Тип делегата | Приклад лямбди |
|---|---|---|---|
|
Фільтрує елементи | |
|
|
Проєктує, перетворює | |
|
|
Сортування за ключем | |
|
|
Перший елемент за умовою | |
|
|
Чи є хоч один елемент за умовою | |
|
|
Чи всі елементи задовольняють умову | |
|
|
Кількість елементів за умовою | |
|
|
Виконати дію для кожного елемента | |
|
|
Видаляє всі елементи за предикатом | |
|
8. Типові помилки та особливості
Одна з найчастіших помилок — забути, що LINQ не змінює вихідну колекцію, а повертає нову послідовність. Тобто після коду var sorted = users.OrderBy(u => u.Age); колекція users залишиться у вихідному порядку! Це може збити з пантелику: іноді здається, що вже все відсортовано — а насправді ні.
Ще нюанс: методи типу Where, Select та інші повертають об’єкти типу IEnumerable<T>. Це «лінива» колекція — реальна обробка починається, коли ви справді починаєте її перераховувати (foreach, ToList() тощо). Тому, якщо хочете матеріалізувати результат, не забудьте викликати ToList() або ToArray():
var sortedList = users.OrderBy(u => u.Age).ToList();
Також варто пам’ятати: якщо лямбда-вираз звертається до змінних поза своєю зоною видимості (замикання), то ці змінні продовжують «жити» в пам’яті доти, доки живе посилання на лямбду. Нічого критичного, але якщо ви використовуєте лямбду всередині довгоживучого об’єкта й захопили в ній «величезний масив», то масив висітиме в пам’яті разом із лямбдою.
І ще: використовуйте промовисті назви параметрів і змінних — це істотно підвищує читабельність, особливо якщо маєте кілька рівнів вкладених лямбд.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ