C# мові делегат (delegate) можна визначити як “тип безпечного для типів вказівника на функцію, який може вказувати на один або декілька методів”. Це нагадує концепцію вказівників на функції в C та C++, але головна відмінність полягає в тому, що C# забезпечує типову безпеку в керованому (managed) середовищі. Це дозволяє зменшити проблеми, такі як несумісність підписів (типи параметрів і повернених значень) або помилки пам'яті.
У підсумку: Delegate зберігає підпис методу (типи параметрів + тип повернення) і дозволяє вибрати метод для виклику під час виконання програми.
🤔 Чому делегати так важливі?
- Механізм зворотного виклику (Callback): Ви можете використовувати делегати для визначення, які методи будуть виконані після завершення довготривалої операції. Наприклад, якщо завантажується файл, можна викликати метод для показу повідомлення після завершення завантаження.
- Система подій (Event System): Основою концепції подій в C# є делегати. Події, які повинні спрацьовувати при натисканні кнопки в користувацькому інтерфейсі (UI) або після завершення асинхронної операції, реалізуються за допомогою делегатів.
- Стратегія (Strategy Pattern): Ви можете вибирати алгоритми в залежності від введення користувача через делегат. Наприклад, якщо у вас є різні стратегії запису на диск, делегат може визначати, яка з них буде використана в процесі виконання.
- Зрозумілий і гнучкий код: Можливість передавати методи, як параметри, підвищує модульність коду. Лямбда-вирази і LINQ також ґрунтуються на цьому підході.
🪄 Оголошення делегатів і приклади
- Як оголосити делегат?
Делегат зазвичай оголошується в просторі імен (поза класом):
public delegate int MyMathDelegate(int x, int y); // Делегат, який представляє метод, що приймає два int і повертає int
Це просте оголошення створює спеціальний тип (MyMathDelegate
), що має підпис (int, int) -> int
.
- Як прив'язати метод до делегата?
public delegate int MyMathDelegate(int x, int y);
class Program
{
static void Main()
{
// Створюємо приклад делегата
MyMathDelegate operation;
// 1) Прив'язуємо до методу додавання
operation = Add;
int sum = operation(5, 3); // 8
Console.WriteLine($"Сума: {sum}");
// 2) Прив'язуємо до методу множення
operation = Multiply;
int product = operation(5, 3); // 15
Console.WriteLine($"Добуток: {product}");
}
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
}
Як бачите, спочатку метод Add
підключений до operation
, а в наступному рядку operation
викликає метод Multiply
. Вся ця гнучкість дозволяє виконувати перехід між методами з підписами, що збігаються, на етапі виконання (runtime).
⚡ Мульти-методні делегати (Multicast Delegates)
Делегат може викликати кілька методів (multi-cast). Ці методи виконуються у вигляді "ланцюга викликів".
public delegate void NotifyDelegate(string message);
class Program
{
static void Main()
{
NotifyDelegate notify = MethodA;
notify += MethodB;
notify += MethodC;
notify("Привіт");
// Викликаються MethodA -> MethodB -> MethodC по черзі.
}
static void MethodA(string msg) => Console.WriteLine("A каже: " + msg);
static void MethodB(string msg) => Console.WriteLine("B каже: " + msg);
static void MethodC(string msg) => Console.WriteLine("C каже: " + msg);
}
У ланцюгу викликів, якщо тип повернення — це void
, то проблем немає, всі методи виконуються послідовно. Однак якщо тип повернення вимагає значення (наприклад, int
), то зазвичай використовується результат останнього доданого методу. Тому, якщо у вас є важливе значення повернення, більш безпечно використовувати делегат, який прив'язаний лише до одного методу, замість мульти-методного підходу.
🔑 Готові типи делегатів: Action, Func, Predicate
Мова C# надає готові generic типи делегатів для найпоширеніших підписів. Це дозволяє не створювати власні делегати для багатьох випадків.
1.
Action
- Представляє методи, що мають тип повернення
void
і можуть приймати від 0 до 16 параметрів. - Наприклад:
Action
для методів, що приймають один параметр типу string і повертаютьvoid
.
Func
- Представляє методи, які повертають значення. Можуть приймати від 0 до 16 параметрів.
- Наприклад:
Func
для методів, що приймають два параметри типу int і повертають значення типу int.
Predicate
- Приймає один параметр і повертає значення типу
bool
. - Наприклад:
Predicate
для методів, що приймають параметр типу int і повертаютьtrue/false
.
// Action
Action greet = (name) => Console.WriteLine("Привіт " + name);
greet("Ахмет"); // "Привіт Ахмет"
// Func
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // 5
// Predicate
Predicate<int> isEven = x => x % 2 == 0;
Console.WriteLine(isEven(10)); // True
Console.WriteLine(isEven(11)); // False
Ці готові типи дуже корисні, особливо в LINQ та при використанні Лямбда-виразів.
📌 Анонімні методи та лямбда-вирази
Користуючись делегатами, замість того, щоб визначати ім’я методу, можна написати анонімний метод або лямбда-вираз (arrow function).
Анонімний метод:
Action helloDelegate = delegate
{
Console.WriteLine("Привіт, Світ!");
};
helloDelegate(); // "Привіт, Світ!"
Лямбда-вираз:
Action helloLambda = (name) =>
{
Console.WriteLine($"Привіт, {name}!");
};
helloLambda("Зейнеп");
🎉 Взаємозв'язок делегатів і подій
У C# структура event є особливим використанням делегатів. До події можна підписуватися тільки за допомогою +=
та -=
; це запобігає випадковому виклику події ззовні.
public delegate void ThresholdReachedHandler(int currentValue);
public class Counter
{
public event ThresholdReachedHandler ThresholdReached;
private int total = 0;
private int threshold = 10;
public void Add(int value)
{
total += value;
if (total >= threshold)
{
// Викликаємо подію, перевіряючи на null за допомогою "?"
ThresholdReached?.Invoke(total);
}
}
}
Приклад використання:
var counter = new Counter();
counter.ThresholdReached += (val) => Console.WriteLine($"Ліміт перевищено! Значення: {val}");
counter.Add(5);
counter.Add(6); // Подія спрацьовує: 11 >= 10
У подіях принцип роботи делегатів працює "за кулісами"; подія просто надає суворіші обмеження доступу.
🤿 Для зацікавлених: більш глибоке використання та важливі моменти
- Управління виключеннями: Якщо один із методів в мульти-методному делегаті викликає помилку, решта методів зазвичай не виконуються. Тому, коли ви створюєте ланцюг методів, можливо, вам буде потрібно обробляти помилки окремо для кожного методу за допомогою try-catch.
- Продуктивність: Виклик делегата може бути трохи дорожчим за виклик звичайного методу. Однак у більшості випадків це не має значення. Для продуктивних додатків варто проводити вимірювання (профілювання).
- Ризик витоків пам'яті: Якщо ви підписуєтеся на події (
+=
), обов'язково відписуйтеся (-=
) після завершення роботи, щоб уникнути неочікуваного збереження об'єктів у пам'яті (затримка збирання сміття). - Асинхронне використання: Делегати за замовчуванням працюють синхронно. Якщо вам потрібно асинхронне виконання, використовуйте
async/await
,Task
або підходи з використаннямThread
/ThreadPool
. - Delegate та Expression Trees: У складніших сценаріях ви можете створювати делегати через expression tree і компілювати їх.
Bu, dinamik kod üretimi ve metot çağrılarında esneklik sunar.
🎯 Узагальнення…
- Оволодівши структурою Delegate, ви зможете легко створювати архітектури, що базуються на callback та подіях.
- Готові типи делегатів, такі як
Action
,Func
таPredicate
, скорочують код та пропонують стандартну структуру. - Використовуючи multicast delegate (ланцюг викликів), звертайте увагу на типи повернення та управління помилками.
- Події — це більш контрольована форма використання делегатів, і вони часто складають основу подійних архітектур у великих проектах.
Після того, як ви освоїте концепцію делегатів, ви зможете набагато легше працювати з такими сферами, як асинхронне програмування, шаблони проектування (особливо Strategy та Observer) та LINQ запити.
📚 Додаткові ресурси
Перекладено з: 🔍Onların da Temsil Edilme İhtiyacı Var! Delegate’e DahaYakından Bakalım.