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> 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-лямбда.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ