8_Делегаты и события

Урок 8 : Делегаты и события

О чём этот урок

Делегаты - это типы для хранения ссылок на методы. События строятся на их основе и позволяют объектам уведомлять друг друга о происходящем, не зная ничего друг о друге.


1. Что такое делегат

Делегат - это как перепрограммируемый пульт дистанционного управления: кнопка (вызов) одна, а что именно произойдёт по нажатию - можно задать или изменить в любой момент.

Тип данных, который хранит ссылку на один или несколько методов с определённой сигнатурой. Это «указатель на метод», оформленный как первоклассный объект.

// 1. Объявляем тип делегата (какую сигнатуру принимает)
delegate int Operation(int a, int b);

// 2. Методы с подходящей сигнатурой
int Add(int a, int b) => a + b;
int Multiply(int a, int b) => a * b;

// 3. Создаём экземпляр делегата и привязываем метод
Operation op = Add;
Console.WriteLine(op(3, 4));   // 7

// Меняем метод на лету
op = Multiply;
Console.WriteLine(op(3, 4));   // 12

2. Обычный вызов vs вызов через делегат

// Обычный вызов - жёстко прописан в коде
int result = Add(5, 3);

// Вызов через делегат - метод можно подставить снаружи
Operation calculate = Add;
int result2 = calculate(5, 3); // то же самое, но гибко

// Практическая польза: можно передавать поведение как аргумент
void ApplyTwice(int x, Operation op)
{
    Console.WriteLine(op(x, x));  // не знаем, какой именно метод - гибко!
}

ApplyTwice(5, Add);       // 10
ApplyTwice(5, Multiply);  // 25
Зачем нужны делегаты?

Делегат позволяет передавать поведение как данные. Это ключевой инструмент для:

  • Callback-функций (вызов после завершения операции)
  • Событийной модели (кнопка нажата → уведомить всех подписчиков)
  • Функционального программирования (фильтрация, сортировка)

3. Сигнатура метода и делегат

Метод подходит делегату, если полностью совпадает сигнатура: возвращаемый тип и типы параметров.

delegate void Printer(string message);   // void, один string
delegate bool Checker(int value);        // bool, один int

void PrintUpper(string msg) => Console.WriteLine(msg.ToUpper()); // подходит
void PrintLog(string msg, int id) { }    // не подходит (2 параметра)
bool IsEven(int n) => n % 2 == 0;       // подходит для Checker

Printer p = PrintUpper;
p("hello"); // HELLO

Checker c = IsEven;
Console.WriteLine(c(4));  // true

Мультикаст (несколько методов в одном делегате)

delegate void Notify(string message);

void LogToConsole(string msg) => Console.WriteLine($"[Console] {msg}");
void LogToFile(string msg)    => Console.WriteLine($"[File] {msg}");

Notify notifier = LogToConsole;
notifier += LogToFile;   // добавляем второй метод

notifier("Игрок победил!"); // вызовутся ОБА метода
// [Console] Игрок победил!
// [File] Игрок победил!

notifier -= LogToFile;   // убираем метод

4. Анонимные методы и лямбды

Не обязательно объявлять отдельный именованный метод - можно написать его прямо на месте.

Анонимный метод (устаревший синтаксис, C# 2)

delegate int Operation(int a, int b);

Operation add = delegate(int a, int b)
{
    return a + b;
};
Console.WriteLine(add(3, 4)); // 7

Лямбда-выражение (современный синтаксис)

// Полная запись
Operation multiply = (int a, int b) => { return a * b; };

// Краткая запись (типы выводятся, одно выражение)
Operation add      = (a, b) => a + b;
Operation subtract = (a, b) => a - b;

Console.WriteLine(add(10, 3));       // 13
Console.WriteLine(multiply(4, 5));   // 20
// Лямбда без параметров
Action greet = () => Console.WriteLine("Привет!");
greet();

// Лямбда с одним параметром (скобки необязательны)
Action<string> print = msg => Console.WriteLine(msg);
print("Hello");

// Лямбда с телом
Func<int, int> factorial = n =>
{
    int result = 1;
    for (int i = 2; i <= n; i++) result *= i;
    return result;
};
Console.WriteLine(factorial(5)); // 120

5. Встроенные делегаты .NET: Func, Action, Predicate

Вместо объявления собственных делегатов можно использовать готовые:

Action - метод без возвращаемого значения (void)

Action greet     = () => Console.WriteLine("Hello!");
Action<string> p = msg => Console.WriteLine(msg);
Action<int, int> show = (a, b) => Console.WriteLine(a + b);

greet();      // Hello!
p("World");   // World
show(2, 3);   // 5

Func - метод с возвращаемым значением

// Последний тип - возвращаемый
Func<int>              getZero  = () => 0;
Func<int, int>         square   = x => x * x;
Func<int, int, int>    add      = (a, b) => a + b;
Func<string, bool>     hasSpace = s => s.Contains(' ');

Console.WriteLine(square(5));       // 25
Console.WriteLine(add(3, 4));       // 7
Console.WriteLine(hasSpace("hi"));  // false

Predicate - метод, возвращающий bool

Predicate<int>    isEven = n => n % 2 == 0;
Predicate<string> isLong = s => s.Length > 5;

Console.WriteLine(isEven(6));       // true
Console.WriteLine(isLong("Hi"));    // false

// Используется в List<T>.FindAll, List<T>.RemoveAll и др.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evens   = numbers.FindAll(isEven);  // { 2, 4, 6 }

6. События (event)

Событие (event) - механизм уведомления: объект сообщает всем заинтересованным сторонам, что что-то произошло.

Разница между event и обычным делегатом-полем

Характеристика Делегат (поле) event
Вызов извне класса Можно Нельзя
Присваивание извне Можно (=) Нельзя (только += и -=)
Подписка извне Можно Можно (+=)
Отписка извне Можно Можно (-=)
class Button
{
    // event - защищённый делегат: подписаться можно, вызвать снаружи - нет
    public event Action Clicked;

    public void SimulateClick()
    {
        Console.WriteLine("Кнопка нажата");
        Clicked?.Invoke();  // ?. - безопасный вызов, если нет подписчиков
    }
}

// Подписка
Button btn = new Button();
btn.Clicked += () => Console.WriteLine("Обработчик 1: обновить UI");
btn.Clicked += () => Console.WriteLine("Обработчик 2: сохранить данные");

btn.SimulateClick();
// Кнопка нажата
// Обработчик 1: обновить UI
// Обработчик 2: сохранить данные

7. Стандартный шаблон событий в .NET

Стандарт .NET предполагает использование EventHandler и EventArgs.

EventArgs - данные события

// Пользовательские данные события
class HealthChangedEventArgs : EventArgs
{
    public int OldHealth { get; }
    public int NewHealth { get; }
    public int Delta => NewHealth - OldHealth;

    public HealthChangedEventArgs(int oldHp, int newHp)
    {
        OldHealth = oldHp;
        NewHealth = newHp;
    }
}

Стандартная сигнатура обработчика

// EventHandler<TEventArgs> - стандартная сигнатура:
// sender: объект, породивший событие
// e: данные события
void OnHealthChanged(object sender, HealthChangedEventArgs e)
{
    Console.WriteLine($"HP: {e.OldHealth} → {e.NewHealth} (Δ{e.Delta})");
}

Полный пример с событием

class Player
{
    private int _health;
    public int MaxHealth { get; }

    // Событие со стандартной сигнатурой
    public event EventHandler<HealthChangedEventArgs> HealthChanged;

    public Player(int maxHealth)
    {
        MaxHealth = maxHealth;
        _health   = maxHealth;
    }

    public int Health
    {
        get => _health;
        private set
        {
            int old = _health;
            _health = Math.Clamp(value, 0, MaxHealth);
            if (old != _health)
                OnHealthChanged(old, _health);  // уведомляем
        }
    }

    public void TakeDamage(int damage) => Health -= damage;
    public void Heal(int amount)       => Health += amount;

    // Защищённый метод вызова события
    protected virtual void OnHealthChanged(int oldHp, int newHp)
    {
        HealthChanged?.Invoke(this, new HealthChangedEventArgs(oldHp, newHp));
    }
}

// Использование
Player player = new Player(100);
player.HealthChanged += (sender, e) =>
{
    var p = (Player)sender;
    Console.WriteLine($"[UI] HP: {e.NewHealth}/{p.MaxHealth}");
    if (e.NewHealth == 0) Console.WriteLine("[UI] Игрок мёртв!");
};

player.TakeDamage(30);  // [UI] HP: 70/100
player.TakeDamage(80);  // [UI] HP: 0/100  →  [UI] Игрок мёртв!

8. Оператор ?. при вызове события

?.Invoke() - безопасный вызов: если на событие никто не подписан (значение null), исключения не будет.

public event Action SomethingHappened;

// Небезопасно (выбросит NullReferenceException если нет подписчиков)
SomethingHappened.Invoke();  // ОПАСНО

// Безопасно
SomethingHappened?.Invoke(); // если null - просто ничего не делает

Итог урока


📋 Перейти к практическим заданиям урока 8

Материалы для дополнительного ознакомления :

Делегаты
Применение делегатов
Анонимные методы
События
Action/Predicate/Func
Ковариантность / Контравариантность