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

Комбінування лямбда-виразів і делегатів

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

1. Вступ

Делегат — це тип, що описує «підпис» функції: які параметри вона приймає і що повертає. Це наче паспорт функції або перепустка до нічного клубу для коду: якщо підпис збігається — прохід відкрито, якщо ні — вхід заборонено.

Приклад:

public delegate int Operation(int a, int b);

Такий делегат позначає функцію, що приймає два int і повертає int. Так виглядало програмування «до лямбд» (працює, але не надто зручно):

Operation add = delegate (int x, int y)
{
    return x + y;
};

З появою лямбд код став лаконічнішим:

Operation add = (x, y) => x + y;

Цікавий факт: у C# лямбда-вираз — це просто спосіб створити «анонімну функцію», яку компілятор автоматично перетворює на екземпляр делегата відповідного типу. Тож зв’язок між ними майже непомітний.

Вбудовані делегати: Func, Action, Predicate

Щоб не створювати безліч власних делегатів для кожної ситуації, у C# є універсальні функціональні типи:

  • Func<T1, T2, ..., TResult> — універсальний делегат, що приймає параметри (T1, T2, ...) і повертає значення типу TResult.
  • Action<T1, T2, ...> — майже те саме, але нічого не повертає (void).
  • Predicate<T> — універсальний делегат, що приймає один параметр і повертає bool (улюбленець усіх фільтрів).
Делегат Тип, що повертається Приклад синтаксису
Func<int, int>
int
Func<int, int> f = x => x + 1;
Action<string>
void
Action<string> a = s => ...;
Predicate<double>
bool
Predicate<double> p = d => ...;

Приклад:

Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;

2. Лямбда-вирази «в конверті» делегата

Передавання лямбди методу, що приймає делегат

Будь-який метод, що приймає делегат, може так само прийняти лямбду, якщо її підпис відповідає очікуваному.

public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
    int result = operation(a, b);
    Console.WriteLine($"Результат: {result}");
}

// Викликаємо з лямбдою
CalculateAndShow(5, 7, (x, y) => x * y); // Результат: 35

Приклад з Action

void ProcessString(string message, Action<string> processor)
{
    processor(message);
}

// Використовуємо лямбду
ProcessString("Привіт, світ!", s => Console.WriteLine(s.ToUpper()));

Приклад з Predicate

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);

У такі моменти стає зрозуміло: навіщо вигадувати власні делегати, якщо вже є перевірені універсальні рішення від Microsoft.

Як працюють делегати й лямбди разом

flowchart TD
    A[Лямбда-вираз] -->|Створює| B((Делегат))
    B -->|Передається як аргумент| C[Метод]
    B -->|Зберігається| D[Властивість/Поле/Колекція]
    C -->|Викликає| E[Код лямбди]

3. Передача лямбди як делегата у власних методах

Якщо ваш метод використовує делегати, лямбда робить застосування максимально гнучким.

Приклад: наш міні-калькулятор

// Визначаємо метод для роботи з операціями
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
    return operation(a, b);
}

// Застосовуємо різні операції (лямбди — в дії!)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);

Console.WriteLine(sum);      // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max);      // 4

Помітно, як елегантно змінюється логіка — без трьох окремих методів.

4. Лямбди й делегати в LINQ (і не лише там)

У LINQ методи очікують «логічну мініфункцію», тобто делегат. Більшість стандартних LINQ-методів приймають такі типи:

  • Func<T, bool> для фільтрації (Where)
  • Func<T, TResult> для трансформацій (Select)
  • Func<T, TKey> для сортувань (OrderBy)
List<string> names = new List<string> { "Анна", "Олег", "Віктор", "Яна" };

// Отримуємо лише імена довші за 3 символи
var longNames = names.Where(name => name.Length > 3);

// Перетворюємо імена у верхній регістр
var upperNames = names.Select(name => name.ToUpper());

У LINQ — «лямбди на повну», адже все, що приймає делегат, можна передати як лямбду!

5. Комбінування: зберігання та передача делегатів-лямбд

Список функцій

Лямбда — це не лише тимчасовий анонімний метод. Ви можете зберігати набір лямбд у колекції й використовувати їх динамічно.

List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
    (x, y) => x + y,
    (x, y) => x - y,
    (x, y) => x * y,
    (x, y) => x / y
};

foreach (var op in operations)
{
    Console.WriteLine(op(10, 2));
}

Такий прийом часто трапляється в тестах, обробниках і плагінах.

Словник делегатів

А тепер, якщо ви прихильник plug-and-play архітектури:

var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
    { "plus", (x, y) => x + y },
    { "minus", (x, y) => x - y },
    { "pow", (x, y) => (int)Math.Pow(x, y) }
};

string command = "pow"; // Імітація користувацького введення

if (mathFuncs.TryGetValue(command, out var operation))
{
    Console.WriteLine(operation(2, 5)); // 32
}
else
{
    Console.WriteLine("Команду не знайдено");
}

6. Корисні нюанси

Лямбда-делегати: повернення делегата з методу

Можна повертати делегати (а отже, і лямбди) з методів — це майже фабрика функцій.

Func<int, int> GetMultiplier(int factor)
{
    // Повертаємо лямбда-делегат, який множитиме на factor
    return x => x * factor;
}

var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15

var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20

Так замикання (closure) стає не іграшкою, а реальним інструментом.

Композиція функцій: Combine, Delegate.Combine і мультиделегати

Іноді потрібно, щоб один «виклик» призвів до виконання відразу кількох функцій. Наприклад, під час обробки подій у GUI: натискаємо кнопку — і одразу кілька обробників реагують.

Action і делегати дають змогу об’єднувати кілька функцій:

Action<string> pipeline = s => Console.WriteLine("Крок 1: " + s);
pipeline += s => Console.WriteLine("Крок 2: " + s.ToUpper());
pipeline += s => Console.WriteLine("Крок 3: " + s.Length);

pipeline("тест");
// Виводить:
// Крок 1: тест
// Крок 2: ТЕСТ
// Крок 3: 4

Зручно для сценаріїв «ланцюжків» або обробки подій.

Важливо: для функцій, що повертають значення, результат буде тільки від останнього виклику («хвіст функції»). Для Action виконуються всі, адже void.

Використання лямбда-делегатів як параметрів зворотного виклику

Класичний сценарій — «викличте мене, коли завершите».

void DownloadFile(string url, Action<string> onFinish)
{
    // Імітація завантаження файлу...
    System.Threading.Thread.Sleep(500);// (Імітація довгої роботи, не використовуйте в реальних GUI-додатках)
    onFinish($"Завантаження '{url}' завершено!");
}

DownloadFile("http://example.com", msg => Console.WriteLine(msg));

У реальних застосунках (наприклад, під час роботи з API, базами, файлами) це типовий шаблон.

Функція вищого порядку: приклад фільтра за предикатом

Лямбда-вирази й делегати дають змогу писати універсальні функції, які приймають інші функції як параметри. Це основа підходу «функції вищого порядку».

List<int> Filter(List<int> source, Predicate<int> condition)
{
    List<int> result = new List<int>();
    foreach (var item in source)
        if (condition(item)) result.Add(item);
    return result;
}

// Використовуємо різні лямбди:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);

Можна навіть створювати «фабрики» лямбд:

Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
    x => x >= min && x <= max;

Predicate<int> inRange = GetRangeChecker(1, 10);

var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10

7. Типові помилки й нюанси комбінування

Якщо підпис параметрів не збігається, компілятор повідомить про помилку (CS1660: "Cannot convert lambda expression").

Лямбда-вираз може неочікувано запам’ятовувати змінні, які потім змінюються (замикання / closure) — будьте уважні до захоплення змінних.

Мультиделегати з типом, що повертається, повертають результат тільки від останнього обробника — часто несподіванка для новачків.

У місцях, де потрібні вирази Expression<Func<int,bool>>, не можна підставити statement-лямбду — потрібна саме expression-лямбда.

1
Опитування
Делегати - тип для лямбда-виразів, рівень 50, лекція 4
Недоступний
Делегати - тип для лямбда-виразів
Взаємозв'язок лямбда-виразів і делегатів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ