JavaRush /Курси /C# SELF /Лямбда-вирази із Func

Лямбда-вирази із Func, Action, Predicate

C# SELF
Рівень 50 , Лекція 1
Відкрита

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());

Візуальна схема

Сигнатура Приклад Опис
Func<int, int>
x => x * 2
Приймає int, повертає int
Func<int, int, int>
(a, b) => a + b
Два int, повертає int
Func<string>
() => "hi"
Не приймає нічого, повертає рядок

Як це виглядає у вашому застосунку

Припустімо, у нашому міні-застосунку (умовний «Довідник користувачів») є список чисел — хочемо застосувати до нього різні обробки. Наприклад, піднести до квадрата або порахувати суму з фіксованим числом:

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

Візуальна схема

Сигнатура Приклад Опис
Action
() => ...
Без параметрів, без повернення значення
Action<int>
x => ...
Один параметр
Action<int, string>
(x, s) => ...
Кілька параметрів

У нашому застосунку

Додаймо до довідника користувачів метод для виведення всіх імен:

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);

Таблиця для наочності

Делегат Сигнатура Приклад лямбди Де використовується
Func<T, U>
T → U
user => user.Name
Select, будь-які перетворення
Action<T>
T → void
user => Console.WriteLine(...)
ForEach, методи-дії
Predicate<T>
T → bool
user => user.Age > 18
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); // можна перетворити через конструктор
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ