JavaRush /Курси /C# SELF /Делегати як тип для лямбда-виразів

Делегати як тип для лямбда-виразів

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

1. Вступ

Уявіть, що у вас є кавомашина. Зазвичай вона просто варить каву й нікого не чіпає, але інколи шеф каже: «А можете, будь ласка, після варіння кави крикнути „Готово!“?» — і тут потрібне налаштування. Саму кавомашину ми не переписуємо. Просто створюємо функцію, яку вона виконає наприкінці.

У C# цю роль виконують делегати — вони дозволяють передавати в методи шматочки коду (методи, лямбди або анонімні методи), щоб той метод викликав їх у потрібний момент. Грубо кажучи, делегат — це тип, який може зберігати посилання на методи з певною сигнатурою.

Визначення делегата

У C# делегат визначається за допомогою ключового слова delegate. Приклад:

// Делегат, який посилається на методи, що приймають int і повертають bool
public delegate bool PredicateInt(int x);

Тепер змінна типу PredicateInt зможе посилатися на будь-який метод (або лямбду!), який приймає один int і повертає bool.

Навіщо потрібні делегати?

  • Передача логіки як аргументу (наприклад, для сортування, фільтрації, обробки подій)
  • Підписка на події (про них поговоримо згодом)
  • Реалізація зворотних викликів (callback)
  • Гнучкі API, де частина поведінки визначається стороною, що викликає

Проста візуальна схема

Тип делегата Сигнатура Приклад виклику
Action
void()
Action act = () => ...
Func<int>
int()
Func<int> f = () => 42
Func<int, bool>
bool(int)
Func<int, bool> p = x => x > 0

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<T1, TResult>
Result(T1)
Перетворення, проєкція
Action<T1>
void(T1)
Побічні ефекти, виведення
Predicate<T>
bool(T)
Фільтрація, пошук

Який тип делегата обрати для лямбда-виразу?

  • Якщо очікується значення, що повертається, беріть 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).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ