Назад Вперед Зміст

Події та делегати

Подієва модель роботи додатків

Один із найсучасніших підходів до побудови додатків – підхід, заснований на генерації та подальшій обробці подій.

Що таке подія

Подія — це перехід об'єкта з одного стану до іншого. Наприклад, натискання клавіші, переміщення курсору миші, зміна властивостей якогось об'єкта

Найпростіша подія — це подія, яка повідомляє про початок або про завершення певної процедури.

Генератор події

Події генеруються у відповідь на дії користувача. Це може бути клацання користувачем по кнопці, введення даних у текстове поле і т.д.

Подія може бути згенерована без взаємодії з користувачем, наприклад, при виконанні деякого методу.

Обробник події

Реакція програми на подію виявляється у виконанні деяких дій. І тому існують обробники подій – методи, які безпосередньо виконують дії.

Підписка на подію

При виникненні однієї і тієї ж події для об'єктів різних класів можуть виконуватися різні дії (викликатися різні обробники).

Наприклад, подія «Дзвінок». Реакція на цю подію:

Як програма дізнається, що подія сталася, і які обробники цієї події в різних класах потрібно викликати?

Тому є щось, що поєднує події та обробники. Це підписка на подію. Під час підписки на подію вказується, у якому класі, які обробники мають виконуватися у відповідь на виникнення події.

Для нашого прикладу, є дві підписки на подію «Дзвінок»:

Від події можна і відписатися. Тоді подія відбувається, але відповідний обробник не виконується.

Делегати

Що таке делегат

Делегат це клас. Об'єкти цього класу – це прототипи функцій, тобто функції з певною сигнатурою.

Сигнатура функції — це поєднання назви типу, який функція повертає плюс назви типів вхідних параметрів (у порядку написання).

Опис делегату

При описі змінної типу делегат вказується ключове слово delegate і заголовок функції, сигнатура якої збігається з типом делегата. Після цього делегат можна використовувати для виклику вказаної функції.

Даний делегат Del може представляти будь-яку функцію типу void без значення, що повертається, і з одним параметром типу string:
public delegate void Del (string message);

Даний делегат MyDel може представляти будь-яку функцію типу void без значення, що повертається і без параметрів:
public delegate void MyDel();

Даний делегат Ddel може представляти будь-яку функцію типу void без значення, що повертається, і з двома параметрами: перший типу int, другий типу double.
public delegate void Ddel(int i, double j);

Що являє собою об'єкт класу делегат

Фундаментальна відмінність змінної типу делегат від раніше розглянутих полягає в тому, що як значення змінної цього типу можна надати метод відповідний певній сигнатурі.

У будь-який метод можна передати як параметр об'єкт класу делегат, тобто можна як параметр передати в метод функцію.

Для створення об'єкта класу делегатів використовують конструктор:
делегат змінна =new делегат(объект.метод);

Метод, який передається як параметр делегата, повинен мати таку саму сигнатуру, як і опис делегата.

Приклад

Нехай у класі MathClass описано два методи з однаковою сигнатурою:

public class MathClass
{
    public void MultiplyNumbers(int m, double n)
    {
        double x = n * m;
        Console.Write(x + " ");
    }

    public void AddNumbers(int m, double n)
    {
        double x = n + m;
        Console.Write(x + " ");
    }
}

У класі Program:

class Program
{
    // описали делегат
    public delegate void Del(int i, double j);

    static void Main(string[] args)
    {
        // створили об'єкт класу MathClass
        MathClass m = new MathClass();

        // створили об'єкт класу Del:
        // як параметр метод MultiplyNumbers
        // класу MathClass, застосований до об'єкта m
        Del d1 = new Del(m.MultiplyNumbers);

        // створили ще один об'єкт класу Del:
        // як параметр метод AddNumbers
        // класу MathClass, застосований до об'єкта m
        Del d2 = new Del(m.AddNumbers);

        // використовуємо об'єкт (метод) d1
        for (int i = 1; i <= 5; i++) { d1(i, 2); }
        Console.WriteLine();

        // використовуємо об'єкт (метод) d2
        for (int i = 1; i <= 5; i++) { d2(i, 2); }

        Console.ReadKey();
    }
}

Об'єкти класу Del це методи. Об'єкт d1 це фактично метод MultiplyNumbers, застосований до об'єкта m, а об'єкт d2, це метод AddNumbers, застосований до того ж об'єкта.

А тепер додамо два методи з іншою сигнатурою. І опшемо другий делегат.

class MathClass
{
    // Метод для множення чисел з виведенням результату
    public void MultiplyNumbers(int n, double m)
    {
        double result = m * n;
        Console.WriteLine("{0} * {1} = {2}", n, m, result);
    }

    // Метод для додавання чисел з виведенням результату
    public void AddNumbers(int n, double m)
    {
        double result = m + n;
        Console.WriteLine("{0} + {1} = {2}", n, m, result);
    }

    // Метод для множення чисел з поверненням результату
    public double MultiplyNumbersWithReturn(int n, double m)
    {
        return m * n;
    }

    // Метод для додавання чисел з поверненням результату
    public double AddNumbersWithReturn(int n, double m)
    {
        return m + n;
    }
}
class Program
{
    // Оголошення делегатів
    public delegate void MyDel(int i, double j);   // Делегат для методів без повернення значення
    public delegate double MyDel2(int i, double j); // Делегат для методів з поверненням double

    static void Main(string[] args)
    {
        MathClass m = new MathClass();
        
        // Створення екземплярів делегатів
        MyDel d1 = new MyDel(m.MultiplyNumbers);  // Делегат для методу множення
        MyDel d2 = new MyDel(m.AddNumbers);       // Делегат для методу додавання
        MyDel2 d3 = new MyDel2(m.MNumbers);       // Делегат для методу множення (з поверненням)
        MyDel2 d4 = new MyDel2(m.ANumbers);       // Делегат для методу додавання (з поверненням)

        // Виклик методів через делегати у циклі
        for (int i = 1; i < 10; i++)
        {
            // Виклик методів без повернення значення
            d1(i, 1.5);    // Виклик MultiplyNumbers(i, 1.5)
            d2(2, i);      // Виклик AddNumbers(2, i)
            
            Console.WriteLine(" ");
            
            // Виклик методів з поверненням значення
            Console.WriteLine("{0}", d3(i, 1.5));  // Виклик MNumbers(i, 1.5)
            Console.WriteLine("{0}", d4(2, i));    // Виклик ANumbers(2, i)
            
            Console.WriteLine("");
            
            // Очікування натискання клавіші для продовження
            Console.ReadKey();
        }
    }
}

/* Пояснення:
   - Делегат MyDel використовується для виклику методів MultiplyNumbers та AddNumbers,
     які виконують обчислення та виводять результат на консоль.
   - Делегат MyDel2 використовується для виклику методів MNumbers та ANumbers,
     які повертають результат обчислень.
   - У циклі відбувається послідовний виклик всіх методів через делегати для значень від 1 до 9.
*/

Делегат із кількома методами (Multicast)

using System;
using System.Text;

namespace ConsoleApp1
{ 
   class Program
   {   
       delegate void Notify();
       static void FirstMethod() => Console.WriteLine("Перший метод!");
       static void SecondMethod() => Console.WriteLine("Другий метод!");
         
       static void Main()
       { 
           Console.OutputEncoding = Encoding.UTF8;
           Notify notify = FirstMethod;
           notify += SecondMethod;
           notify();
       }
   }
}

У .NET є кілька вбудованих делегатів, які використовуються у різних ситуаціях. Найчастіше використовуються:

Передача методів як параметри

  1. Delegate — гнучкість,але потребує явного визначення.
  2. Action<T> — якщо метод нічого не повертає.
  3. Func<T, TResult> — якщо метод щось повертає.

Передаємо метод в метод через делегат

using System;

class Program
{
    // Визначення делегату
    delegate void Message();

    static void Day() { Console.WriteLine("Доброго дня!"); }
    static void Evening() { Console.WriteLine("Доброго вечора!"); }

    static void Hi(out Message del)
    {
        if (DateTime.Now.Hour < 12)
        {
            del = Day;
        }
        else
        {
            del = Evening;
        }
        del(); // Визов методу
    }

    static void Main()
    {
        Message del;
        Hi(out del);
    }
}

Приклад з Action<T>

class Program
{
    // Метод для привітання дня
    static void Day() 
    { 
        Console.WriteLine("Добрий день!"); 
    }

    // Метод для привітання вечора
    static void Evening() 
    { 
        Console.WriteLine("Добрий вечір!"); 
    }

    // Метод для вибору привітання за поточним часом
    static void Hi(out Action del) // Використовуємо out для передачі за посиланням
    {
        if (DateTime.Now.Hour < 12)
        {
            del = Day; // Присвоюємо метод Day делегату
        }
        else
        {
            del = Evening; // Присвоюємо метод Evening делегату
        }
        del(); // Викликаємо обраний метод через делегат
    }

    static void Main()
    {
        Action del; // Оголошуємо змінну делегата
        Hi(out del); // Викликаємо метод Hi, який ініціалізує делегат
    }
}

Приклад з Func<T, TResult>

using System;

class Program
{
    // Метод для обчислення квадрата числа
    static double Square(double x)
    {
        return x * x;
    }

    // Метод для обчислення квадратного кореня з експоненти числа
    static double MyExpr(double x)
    {
        return Math.Sqrt(Math.Exp(x));
    }

    // Метод для виведення результату операції
    static void ShowResult(Func operation, double num)
    {
        try
        {
            double result = operation(num);
            Console.WriteLine($"Результат операції для числа {num}: {result:F4}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Помилка при обчисленні: {ex.Message}");
        }
    }

    static void Main()
    {
        // Демонстрація роботи з різними методами
        ShowResult(Square, 5);    // Квадрат числа 5
        ShowResult(MyExpr, 0);    // Корінь з експоненти 0
        ShowResult(MyExpr, -3);   // Корінь з експоненти -3
        ShowResult(Math.Sin, Math.PI/2);  // Використання вбудованого методу Math.Sin
    }
}

Програма демонструє потужність делегатів у C#, дозволяючи передавати різні математичні операції як параметри. Цей підхід особливо корисний для створення гнучких математичних бібліотек.

Результат операції для числа 5: 25,0000
Результат операції для числа 0: 1,0000
Результат операції для числа -3: 0,2231
Результат операції для числа 1,5707963267949: 1,0000
using System;

class Program
{
    // Метод додавання двох чисел
    static int Add(int a, int b) => a + b;
    
    // Метод піднесення числа до квадрату
    static int Square(int x) => x * x;
    
    // Метод формування привітання
    static string Greet(string name) => $"Привіт, {name}!";
    
    // Виведення результату бінарної операції
    static void ShowResult(Func operation, int x, int y)
    {
        Console.WriteLine($"Результат операції: {operation(x, y)}");
    }
    
    // Виведення результату унарної операції
    static void PrintResult(Func operation, int num)
    {
        Console.WriteLine($"Результат: {operation(num)}");
    }
    
    // Виведення персоналізованого повідомлення
    static void ShowMessage(Func messageFunc, string name)
    {
        Console.WriteLine(messageFunc(name));
    }

    static void Main()
    {
        // Демонстрація роботи з різними методами через делегати
        ShowResult(Add, 3, 7);            // Виклик методу додавання
        PrintResult(Square, 5);           // Виклик методу піднесення до квадрату
        ShowMessage(Greet, "Іван Пупкін"); // Виклик методу привітання
        
        // Додатковий приклад з лямбда-виразом
        PrintResult(x => x * x * x, 3);   // Обчислення куба числа
    }
}

Демонструє використання різних типів делегатів (Func)

Показує передачу методів як параметрів

Ілюструє використання лямбда-виразів

Результат операції: 10
Результат: 25
Привіт, Іван Пупкін!
Результат: 27

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

Події

Подія являє собою повідомлення, що надсилається об'єктом, щоб сигналізувати про вчинення будь-якої дії. Ця дія може бути викликана в результаті взаємодії з користувачем, наприклад, при натисканні кнопки миші, або може бути обумовлено логікою роботи програми.

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

Події мають такі властивості:

При обміні подіями класу відправника подій не відомий об'єкт або метод, який отримуватиме (оброблятиме) сформовані відправником події. Необхідно, щоб між джерелом та одержувачем події був посередник. Саме для цього використовуються делегати.

Опис події

В основі механізму обробки подій лежать делегати, тому, перш ніж оголосити подію, необхідно оголосити делегат. Оголошення події схоже на оголошення змінної типу делегата, але з додаванням на початку ключового слова event. Для оголошення події використовується наступний синтаксис:
event делегат подія;

Генерація події

Щоб згенерувати подію, вказується ім'я події, а потім у круглих дужках через кому перераховуються параметри події.

Перш ніж запустити подію, рекомендується перевірити, чи не дорівнює null змінна, що описує подію.

Підписка та скасування підписки на події

Підписка на подію обов'язково має виконуватися до генерації самої події.

Для підписки на подію для будь-якого об'єкта використовується оператор +=, після якого за допомогою ключового слова new викликається конструктор делегата, як параметр якого вказується ім'я методу, який представляє собою обробник події.
об'єкт1.подія+=new делегат(об'єкт2.метод);

Тут:

об'єкт1 – об'єкт, який ініціює подію;

объект2 – об'єкт, який підписується на подію.

метод – обробник події для об'єкт2.

Для скасування підписки на події використовується та сама конструкція, тільки з оператором -=

Приклад

⏰ Подійна модель: Будильник і Собака

🔗 Схема взаємодії класів

Alarm (Будильник)
⬇️ викликає подію Ring
Dog (Собака)
⬇️ обробляє через WakeUp()
Console.WriteLine("Шарик прокинувся!")

🧠 C# Код


class Alarm
{
    public event Action Ring;

    public void Start()
    {
        Console.WriteLine("⏰ Будильник сработал!");
        Ring?.Invoke();
    }
}

class Dog
{
    public string Name { get; }

    public Dog(string name)
    {
        Name = name;
    }

    public void WakeUp()
    {
        Console.WriteLine($"😴 {Name} прокинувся!");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.OutputEncoding = Encoding.UTF8;
        Alarm alarm = new Alarm();
        Dog dog = new Dog("Шарик");
        alarm.Ring += dog.WakeUp;
        alarm.Start();
        Console.ReadLine();
    }
}

📚 Пояснення

Світлофор — події та делегати

Клас TrafficLight моделює зміну сигналів світлофора, де подія повідомляє про зміну стану, а підписані методи реагують на неї.

using System;

class TrafficLight
{
    public delegate void LightChanged(string color);
    public event LightChanged OnChange;

    private string[] colors = { "Червоний", "Жовтий", "Зелений" };

    public void Start()
    {
        foreach (var color in colors)
        {
            Console.WriteLine($"Зміна сигналу на: {color}");
            OnChange?.Invoke(color);
            System.Threading.Thread.Sleep(1000);
        }
    }
}

class Program
{
    static void ShowMessage(string color)
    {
        Console.WriteLine($"Сигнал: {color}");
    }

    static void Main()
    {
        TrafficLight light = new TrafficLight();
        light.OnChange += ShowMessage;
        light.Start();
    }
}

У цьому прикладі подія OnChange викликається щоразу, коли світлофор змінює колір. Метод ShowMessage реагує на подію.

Пояснення виразу OnChange?.Invoke(color);

1. Що це таке?

Це сучасний і безпечний спосіб виклику події у C#. Він означає:

"Якщо є хоча б один підписник на подію OnChange, тоді викликати її та передати значення color."

2. Поділ на частини

3. Еквівалентний довгий запис

if (OnChange != null)
{
    OnChange(color);
}

Цей код означає те саме, але займає більше місця. Саме тому використовують коротку форму з ?.Invoke().

4. Навіщо використовувати ?.Invoke?

5. Простий приклад

public event Action<string> OnChange;

public void Trigger(string color)
{
    OnChange?.Invoke(color);
}

6. Результат

Якщо метод LogColor підписаний на подію:

OnChange += LogColor;

То при виклику:

OnChange?.Invoke("Червоний");

Виконається:

void LogColor(string color)
{
    Console.WriteLine($"Колір: {color}");
}

Ще раз порядок написання коду програми:

  1. Моделювання ситуації:
  2. Оформлення події:
  3. Підписка на подію:

Назад Вперед Зміст