1. Вступ
Цікаво, що лямбда-вираз сам по собі — як рецепт без каструлі: він описує, що робити, але потребує «контейнера», аби «жити» в коді. У C# для цього є готові універсальні типи делегатів: Func, Action і Predicate.
Уявіть їх як зручні формочки для ваших лямбд — беріть потрібну й використовуйте свою логіку. Не потрібно оголошувати власні типи делегатів, коли під рукою вже є все необхідне.
Трохи історії
Раніше, коли виникала потреба передати функцію як параметр, доводилося оголошувати власний тип делегата. Це було довго й клопітно, і виглядало так:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Коли в C# з’явилися Func, Action і Predicate, про ручне оголошення делегатів можна було забути у 90 % випадків. Тепер це виглядає набагато простіше й універсальніше:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — функція з поверненням
Синтаксис і призначення
Func — це узагальнений делегат (generic delegate), який приймає від 0 до 16 параметрів і повертає значення.
Func<int, int, int> sum = (x, y) => x + y;
Func<тип1, тип2, …, типN, TResult> — усі параметри до останнього — це аргументи, останній — тип, що повертається.
Приклади
1. Сума двох чисел
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Піднесення числа до квадрата
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Без параметрів
Func<string> greet = () => "Привіт, лямбда!";
Console.WriteLine(greet());
Візуальна схема
| Сигнатура | Приклад | Опис |
|---|---|---|
|
|
Приймає int, повертає int |
|
|
Два int, повертає int |
|
|
Не приймає нічого, повертає рядок |
Як це виглядає у вашому застосунку
Припустімо, у нашому міні-застосунку (умовний «Довідник користувачів») є список чисел — хочемо застосувати до нього різні обробки. Наприклад, піднести до квадрата або порахувати суму з фіксованим числом:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — дія без повернення
Action — універсальний делегат для методів, які щось роблять (наприклад, друкують у консоль), але нічого не повертають.
Може приймати від 0 до 16 параметрів, але ніколи нічого не повертає — завжди void.
Приклади
1. Виведення в консоль
Action<string> print = text => Console.WriteLine("Дані: " + text);
print("Привіт, світ!");
2. Дія без параметрів
Action greet = () => Console.WriteLine("Ласкаво просимо!");
greet();
3. Дія з кількома параметрами
Action<int, int> showSum = (a, b) => Console.WriteLine($"Сума: {a + b}");
showSum(2, 3); // Сума: 5
Візуальна схема
| Сигнатура | Приклад | Опис |
|---|---|---|
|
|
Без параметрів, без повернення значення |
|
|
Один параметр |
|
|
Кілька параметрів |
У нашому застосунку
Додаймо до довідника користувачів метод для виведення всіх імен:
List<string> names = new() { "Анна", "Борис", "Віка" };
Action<string> printName = name => Console.WriteLine("Користувач: " + name);
names.ForEach(printName);
// або так: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — так чи ні?
Коли потрібна функція, яка поверне тільки true або false для одного параметра, використовуйте Predicate<T>. Це делегат, що приймає один параметр і повертає bool.
Predicate — офіційна «нам потрібна булева перевірка» обгортка для лямбди.
Приклади
1. Перевірити, чи число більше за 5
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Використання з методом List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // використовується Predicate<int>
Console.WriteLine(found); // 7
3. Чи всі дорослі?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll приймає Predicate<int>
У чому різниця з Func<T, bool>?
Фактично вони повністю взаємозамінні. Навіть документація Microsoft зазначає: «Predicate<T> — це просто Func<T, bool> для спеціальних API». Водночас окремі методи стандартної бібліотеки очікують саме Predicate.
5. Як лямбди «вписуються» в Func, Action, Predicate
Коли ви пишете лямбду, C# аналізує її форму: якщо вона відповідає потрібному делегату, її можна підставити.
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Скрізь — лямбда. Та під капотом працюють три різні делегати з різними сигнатурами.
Застосування на прикладі «реального» коду
List<User> users = new() {
new User("Анна", 24),
new User("Борис", 17),
new User("Віка", 31),
};
// Функція, яка повертає лише дорослих користувачів (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Список дорослих: " + string.Join(", ", adults.Select(u => u.Name)));
А якщо хочемо вивести імена всіх користувачів через Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Отримати їхні імена (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Таблиця для наочності
| Делегат | Сигнатура | Приклад лямбди | Де використовується |
|---|---|---|---|
|
T → U | |
Select, будь-які перетворення |
|
T → void | |
ForEach, методи-дії |
|
T → bool | |
Find, Exists, фільтри |
6. Приклади з внутрішнім застосунком: крок за кроком
Давайте розширимо наш невеликий застосунок «Довідник користувачів». Нехай у нас є клас User:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — перевіряємо, чи користувач повнолітній
Func<User, bool> isAdult = user => user.Age >= 18;
Використовуємо в LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — шукаємо активного користувача
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — деактивуємо користувача
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — отримуємо короткий опис
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
Усі ці лямбди — цілком повноцінний «код як дані», який можна передавати в методи, зберігати у змінних і комбінувати.
7. Неочевидні нюанси та типові помилки
1. Лямбда має відповідати сигнатурі делегата. Якщо сигнатура не збігається, буде помилка компіляції.
Func<int, string> wrong = x => x * 2; // помилка: очікується string, отримано int
// Правильно:
Func<int, string> right = x => (x * 2).ToString();
2. Не забувайте про void vs return. Action не повертає значення: спроба написати щось на кшталт Action<int> a = x => x * x; не спрацює, адже лямбда повертає значення, хоча не повинна.
3. Predicate<T> і Func<T, bool> часто взаємозамінні, але не завжди. Іноді методи колекцій очікують саме Predicate<T>, іноді — Func<T, bool>. Пряме присвоєння не працює без явного обгортання.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // помилка
// Але:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // можна перетворити через конструктор
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ