1. Вступ
Уявіть, що у вас є кавомашина. Зазвичай вона просто варить каву й нікого не чіпає, але інколи шеф каже: «А можете, будь ласка, після варіння кави крикнути „Готово!“?» — і тут потрібне налаштування. Саму кавомашину ми не переписуємо. Просто створюємо функцію, яку вона виконає наприкінці.
У C# цю роль виконують делегати — вони дозволяють передавати в методи шматочки коду (методи, лямбди або анонімні методи), щоб той метод викликав їх у потрібний момент. Грубо кажучи, делегат — це тип, який може зберігати посилання на методи з певною сигнатурою.
Визначення делегата
У C# делегат визначається за допомогою ключового слова delegate. Приклад:
// Делегат, який посилається на методи, що приймають int і повертають bool
public delegate bool PredicateInt(int x);
Тепер змінна типу PredicateInt зможе посилатися на будь-який метод (або лямбду!), який приймає один int і повертає bool.
Навіщо потрібні делегати?
- Передача логіки як аргументу (наприклад, для сортування, фільтрації, обробки подій)
- Підписка на події (про них поговоримо згодом)
- Реалізація зворотних викликів (callback)
- Гнучкі API, де частина поведінки визначається стороною, що викликає
Проста візуальна схема
| Тип делегата | Сигнатура | Приклад виклику |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. Як лямбда перетворюється на делегат
Синтаксис
Коли ви пишете лямбда-вираз, наприклад, x => x > 5, ви фактично створюєте об’єкт делегата. Лямбда не «живе» у вакуумі: їй обов’язково потрібен тип (хтось має знати, який у неї набір параметрів і що вона повертає). Тому лямбда-вираз у C# завжди неявно (або явно) перетворюється на делегат.
Приклад 1: Зв’язування делегата з методом
// Явно визначаємо делегат
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// Присвоюємо лямбду змінній типу MyPredicate
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
Приклад 2: Використання стандартних делегатів
C# містить набір стандартних узагальнених делегатів: Action, Func<>, Predicate<>. Вони використовуються майже всюди, де ви застосовуєте лямбди в коді.
// Використовуємо Func
Func
isPositive = number => number > 0; Console.WriteLine(isPositive(-5)); // false
3. Стандартні делегати: Func, Action, Predicate
Func<...>
Використовується для методів, які щось приймають і щось повертають.
Сигнатура:
— Останній тип — це значення, що повертається; решта до нього — типи параметрів, наприклад:
Func<int, string> — приймає int, повертає string
Func
intToString = number => "Число: " + number; Console.WriteLine(intToString(7)); // "Число: 7"
Action<...>
Використовується, коли потрібно щось виконати, але нічого не повертати (void).
Action
printHello = name => Console.WriteLine("Привіт, " + name + "!"); printHello("Василь"); // "Привіт, Василь!"
Predicate<T>
Фактично це скорочення для Func<T, bool>. Використовується, коли потрібна логічна перевірка над об’єктом (true/false).
Predicate
isOdd = x => x % 2 != 0; Console.WriteLine(isOdd(3)); // true
Візуальна шпаргалка
| Делегат | Сигнатура | Застосування |
|---|---|---|
|
|
Перетворення, проєкція |
|
|
Побічні ефекти, виведення |
|
|
Фільтрація, пошук |
Який тип делегата обрати для лямбда-виразу?
- Якщо очікується значення, що повертається, беріть Func<...>
- Якщо метод нічого не повертає (void), використовуйте Action<...>
- Якщо потрібна перевірка умови, використовуйте Predicate<T>
Приклад: Фільтрація списку
List
numbers = new List
{ 1, 2, 3, 4, 5, 6 }; // Очікує Predicate
List
evenNumbers = numbers.FindAll(x => x % 2 == 0); Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. Корисні нюанси
Лямбда-вирази і методи колекцій: що під капотом?
Коли ви викликаєте метод колекції, передаючи лямбду, наприклад:
var adults = users.Where(u => u.Age >= 18);
Метод Where очікує аргумент типу Func<T, bool>. Тобто ваша лямбда u => u.Age >= 18 компілятором перетворюється на об’єкт делегата такого типу.
Блок-схема: як це працює
Ваша лямбда --> Компілятор C# --> Об’єкт делегата (Func
) (u => u.Age >= 18) [тип відомий] (Готовий до виклику у Where())
Детальніше про типізацію: виведення типу
Зазвичай тип делегата компілятор виводить автоматично, якщо він зрозумілий з контексту. Наприклад, для методу List<T>.Find очікується Predicate<T>, і компілятор знає тип параметра з сигнатури методу.
List
words = new List
{ "one", "two", "three" }; var result = words.Find(word => word.Length == 5); // Find очікує Predicate
Console.WriteLine(result); // "three"
Якщо ж контекст не очевидний, доведеться допомогти компілятору:
// Явно вказуємо тип
Func
check = x => x > 10;
Делегати, що повертаються: фабрика функцій
Іноді методи можуть повертати делегати — тобто створювати «фабрики функцій». Це зручно для генерації динамічної поведінки.
// Функція, що повертає делегат (лямбду)
Func
GetMultiplier(int factor) { return x => x * factor; } var times3 = GetMultiplier(3); Console.WriteLine(times3(5)); // 15
Це працює, бо лямбда (x => x * factor) захоплює змінну factor із зовнішнього контексту (closure/замикання) і повертається як об’єкт типу Func<int, int>.
5. Помилки й непорозуміння з делегатами та лямбдами
Невідповідність сигнатур
Компілятор не дасть записати лямбду в делегат, якщо параметри або значення, що повертається, не збігаються.
Func
f = x => "Не можна повернути рядок!"; // Помилка компіляції
Помилка при спробі використати лямбду без делегата
Не можна просто написати лямбду й спробувати викликати її без типу:
// Це не спрацює — компілятор не може вивести тип
// var myFunc = x => x * 2; // Помилка CS0815
// myFunc(10);
Щоб це працювало, потрібно явно вказати тип або надати контекст:
Func
myFunc = x => x * 2; Console.WriteLine(myFunc(10)); // 20
Плутанина між Action, Func і Predicate
Іноді можна випадково обрати не той тип делегата, через що виникне помилка відповідності сигнатури. Памʼятайте просте правило: Func — коли є результат, Action — коли результату немає (void), Predicate<T> — коли потрібна логічна відповідь (bool).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ