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 - просто ничего не делает
Итог урока
- Делегат - тип для хранения ссылок на методы с определённой сигнатурой
- Обычный вызов фиксирован; делегат позволяет передавать поведение как данные
- Сигнатура метода и делегата должны совпадать: возвращаемый тип + типы параметров
- Лямбда
(a, b) => a + b- анонимный метод прямо в месте использования - Встроенные делегаты:
Action(void),Func<..., TResult>(с возвратом),Predicate<T>(bool) - event - защищённый делегат: нельзя вызвать и перезаписать снаружи
- Стандарт:
EventHandler<TEventArgs>+ класс наследникEventArgs ?.Invoke()- безопасный вызов при отсутствии подписчиков
📋 Перейти к практическим заданиям урока 8
Материалы для дополнительного ознакомления :
Делегаты
Применение делегатов
Анонимные методы
События
Action/Predicate/Func
Ковариантность / Контравариантность