1. Колбеки й асинхронне програмування
Колбек (callback) — це механізм передавання методу, який має бути викликаний після завершення певної операції. Дуже часто використовується в асинхронних операціях, таймерах, обробці даних, користувацьких інтерфейсах.
Приклад 1: Асинхронна операція з колбеком
Припустимо, у нас є застосунок, де користувач вводить запит, а результат отримуємо із затримкою (наприклад, з інтернету). Після отримання даних хочемо оновити екран.
// Делегат для зворотного виклику
public delegate void DataReceivedHandler(string result);
// Механізм асинхронного завантаження даних (імітація)
public void DownloadDataAsync(DataReceivedHandler callback)
{
// Припустимо, завантаження займає час (імітуємо через таймер)
Task.Delay(1000).ContinueWith(_ =>
{
string data = "Результати пошуку: <дані>";
callback(data); // Виклик колбек-делегата
});
}
// Використання:
DownloadDataAsync(result =>
{
Console.WriteLine("Отримано: " + result);
});
Цей підхід дає змогу писати гнучкий код, де логіка після отримання результату повністю відокремлена від механіки отримання даних.
2. Делегати як параметри методів: стратегії й компаратори
Поширене завдання: дати змогу користувачеві передати «логіку» (функцію) у ваш метод, щоб він сам визначив, як порівнювати, фільтрувати чи перетворювати елементи.
Приклад 2: Реалізація патерну Strategy через делегати
Припустімо, у нас є сортування, але хочемо, щоб користувач міг сортувати по-різному — за іменем, датою, розміром тощо.
public delegate bool CompareFunc(int a, int b);
public void BubbleSort(int[] arr, CompareFunc compare)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length - 1; j++)
{
if (compare(arr[j], arr[j + 1]))
{
// Міняємо місцями
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Сортування від більшого до меншого
CompareFunc descending = (a, b) => a < b;
// Використання
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Виведе: 4, 3, 2, 1
Такий прийом — універсальний спосіб вбудовувати свою «стратегію» у чужий код без змін його вихідного коду.
3. Анонімні методи, лямбда-вирази й делегати
З розвитком C# стало незручно для кожного завдання оголошувати окремий клас чи метод. На щастя, з’явилися анонімні методи та лямбда-вирази, які дають змогу створювати делегати «на льоту».
Приклад 3: Лямбда як делегат
Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15
Приклад 4: Вибір операції за назвою (switch + делегати)
Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"
switch (userInput)
{
case "sum": op = (a, b) => a + b; break;
case "sub": op = (a, b) => a - b; break;
case "mul": op = (a, b) => a * b; break;
case "div": op = (a, b) => a / b; break;
default: throw new Exception("Невідома операція!");
}
Console.WriteLine(op(6, 2));
Не забувайте про валідацію вхідних даних — делегати тут дають гнучкість і читабельність.
4. Делегати й ланцюжки обробки («ланцюг обовʼязків»)
Оскільки делегати підтримують багатоадресність, можна легко будувати ланцюжки обробників.
Приклад 5: Ланцюжок виклику фільтрів
Уявімо, що в нас є «фільтри», які мають обробити рядок.
public delegate string StringFilter(string input);
string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();
StringFilter filters = RemoveDigits;
filters += ToUpper;
// Делегат пропустить рядок через усі фільтри
string text = "Привіт123";
foreach (StringFilter filter in filters.GetInvocationList())
{
text = filter(text);
}
Console.WriteLine(text); // Виведе: "ПРИВІТ"
Важливо: якщо викликати просто filters(text), буде повернене значення лише останнього обробника, а не всього ланцюжка! Якщо потрібне «протікання» значення, використовуйте явну ітерацію через GetInvocationList(), як показано вище.
5. Делегати для динамічного звʼязування поведінки «на льоту»
Раніше, щоб замінити одну поведінку на іншу, доводилося створювати окремі класи й інтерфейси. З делегатами та лямбда-виразами значну частину «дрібного» поліморфізму можна реалізувати функціями.
Приклад 6: Поведінка робота з динамічною командою
public class Robot
{
public event Action<string>? OnCommandReceived;
public void ReceiveCommand(string command)
{
OnCommandReceived?.Invoke(command);
}
}
// Використання:
var robot = new Robot();
robot.OnCommandReceived += cmd => Console.WriteLine($"Робот виконує: {cmd}");
robot.OnCommandReceived += cmd =>
{
if (cmd == "Увімкнутися")
Console.WriteLine("Завантаження системи...");
};
// Пробуємо
robot.ReceiveCommand("Увімкнутися");
robot.ReceiveCommand("Переміститися вперед");
Такий прийом часто використовують у тестах, прототипах, у DI-контейнерах і для передавання бізнес-логіки через параметри.
6. Делегати як підписки на стан
Припустімо, у нас є клас, що зберігає певний стан, і при його зміні хочемо сповістити всіх підписників. Завдяки делегатам і подіям це елементарно.
Приклад 7: Клас із підпискою на зміну
public class Notifier<T>
{
private T _value = default!;
public event Action<T>? ValueChanged;
public T Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
ValueChanged?.Invoke(_value);
}
}
}
}
// Використання:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Нове значення: {v}");
intValue.Value = 5; // Спрацьовує подія
intValue.Value = 10;
Такий підхід — практично «реактивне програмування на мінімалках», основа для MVVM, data binding і багатьох сучасних UI‑фреймворків.
7. Делегати, замикання й лексична область видимості
Лямбда-вирази й анонімні методи можуть захоплювати змінні з навколишнього контексту (closure). Це зручно, але інколи призводить до неочікуваних помилок.
Приклад 8: Захоплення змінної і «пастка» циклу
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
a(); // Виведе тричі 3 (!)
Чому? Бо замикання посилається на одну й ту саму змінну i, яка після циклу дорівнює 3. А якщо треба запам’ятати саме значення 0, 1, 2?
for (int i = 0; i < 3; i++)
{
int loopValue = i; // «заморожуємо» поточне значення
actions[i] = () => Console.WriteLine(loopValue);
}
Тепер код працює, як очікується. Такі пастки — одна з найчастіших помилок у новачків під час використання лямбда-виразів!
8. Комбінування делегатів
Багатоадресні делегати (multicast) містять список методів, і можна додавати (+=) або видаляти (-=) обробники.
Особливість: видаляються за посиланням і сигнатурою
void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");
Action a = Handler1;
a += Handler2;
a -= Handler1; // Залишить лише Handler2
a?.Invoke(); // Виведе "2"
Приклад: динамічне керування обробниками
Action a = Handler1;
a += Handler1;
a -= Handler1; // Тепер залишається ОДИН Handler1 у списку!
9. Делегати для розширюваності та інверсії керування (IoC)
У великих застосунках часто потрібно, щоб компоненти вміли «викликати» сторонній код, залишаючись при цьому незалежними. Делегати допомагають вписати «розширення», плагіни та зворотні виклики без «tight coupling».
Приклад: Інʼєкція поведінки в конструктор
public class Greeter
{
private readonly Func<string> _getName;
public Greeter(Func<string> getName)
{
_getName = getName;
}
public void Greet() => Console.WriteLine($"Привіт, {_getName()}!");
}
// Ін’єкція різної поведінки:
var greeter1 = new Greeter(() => "Аня");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());
greeter1.Greet(); // "Привіт, Аня!"
greeter2.Greet(); // "Привіт, 14:35!"
У реальному житті такий прийом використовують під час написання тестованого й підтримуваного коду.
10. Корисні нюанси
Делегати в стандартних інтерфейсах і LINQ
З делегатами ви зустрінетеся неминуче, якщо працюєте з LINQ, колекціями, асинхронністю.
- Багато методів на кшталт List<T>.Find, Array.Sort, Where, Select приймають делегати (Func<T, bool>, Comparison<T> та ін.).
- LINQ-методи дають змогу передавати логіку фільтрації, перетворення, агрегації — без створення окремих класів.
Приклад: Компаратор для сортування об’єктів
var people = new[] { "Іван", "Марія", "Петро" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Іван, Петро, Марія
Делегати й карирування (часткове застосування аргументів)
За допомогою анонімних методів/лямбда-виразів можна «зафіксувати» частину параметрів і отримати нову функцію.
Приклад: Часткове застосування
Func<int, int, int> sum = (x, y) => x + y;
// Створюємо функцію, яка завжди додає 10
Func<int, int> add10 = y => sum(10, y);
Console.WriteLine(add10(5)); // 15
Особливість порівняння делегатів
У C# делегати можна порівнювати між собою на рівність (==), якщо в них однаковий список викликів (invocation list).
void Handler1() { }
void Handler2() { }
Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True
Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True
Але якщо делегат побудовано на анонімному методі чи лямбді — порівнюються вже самі екземпляри.
Серіалізація делегатів
Делегати можна серіалізувати, але лише якщо методи, на які вони посилаються, визначені в класах, які можна серіалізувати, і всі типи доступні. Починаючи з .NET 8, BinaryFormatter за замовчуванням вимкнений і вважається застарілим, а в майбутніх версіях буде повністю видалений; серіалізація делегатів у продакшн-завданнях практично не використовується.
Взаємодія делегатів і подій: де делегат, а де подія?
- Делегат — це тип/змінна, яку можна викликати явно.
- Подія (event) — спосіб обмежити доступ до делегата: ззовні можна лише підписуватися/відписуватися (+=/-=), а викликати — тільки зсередини класу.
- Подія завжди делегатного типу, але не кожний делегат — подія.
Як застосовувати? Якщо хочете, щоб логіку можна було визначити поза межами класу, використовуйте делегати. Якщо треба контролювати підписку/відписку і захищати змінну — використовуйте подію.
11. Типові помилки під час роботи з делегатами
Помилка № 1: Плутанина між делегатом і подією.
Використання публічного поля-делегата (public Action MyAction;) замість події (public event Action MyAction;) ламає інкапсуляцію. Зовнішній код може випадково або навмисно перезаписати всіх підписників (instance.MyAction = null;) або викликати їх напряму, що порушує логіку класу.
Помилка № 2: Некоректна робота з поверненими значеннями у багатоадресних делегатів.
Якщо делегат повертає значення (наприклад, Func<string, int>), при звичайному виклику (myDelegate("test")) повернеться результат лише останнього методу в ланцюжку. Щоб отримати результати всіх підписників, потрібно ітеруватися по списку викликів через GetInvocationList().
// Приклад перебору результатів усіх підписників
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
var r = ((Func<string, int>)d)("test");
Console.WriteLine(r);
}
Помилка № 3: Захоплення змінної циклу в замиканні.
Класична пастка для новачків: лямбда, створена всередині циклу for, захоплює саму змінну-ітератор, а не її поточне значення.
// Неправильно: усі дії виведуть останнє значення i
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
// Правильно: створюємо локальну копію для кожної ітерації
for (int i = 0; i < 3; i++)
{
int copy = i;
actions[i] = () => Console.WriteLine(copy);
}
Помилка № 4: Створення витоків пам’яті.
Якщо метод екземпляра підписується на делегат довгоживучого об’єкта, але не відписується, виникає витік. Довгоживучий об’єкт тримає посилання на підписника, і збирач сміття не може його видалити. Слідкуйте за відпискою, особливо в класах з обмеженим життєвим циклом (наприклад, UI‑компонентах).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ