Урок 8.5 — Делегаты и события
Делегаты — ссылки на методы. События — подписка и уведомление.
Представь, что у тебя есть список дел на день. Ты можешь дать этот список другу, чтобы он выполнил все задачи. В C# делегат — это тип, который хранит ссылку на метод. Ты можешь передать метод как аргумент, сохранить его в переменной, а позже вызвать. Событие — это механизм, позволяющий объекту сообщать другим объектам о том, что что-то произошло. Это основа UI-программирования и многих паттернов.
Что такое делегат
Делегат (delegate) — это тип, который определяет сигнатуру метода (возвращаемый тип и параметры) и может хранить ссылку на любой метод с этой сигнатурой. Делегаты в C# — это аналог указателей на функции из C/C++, но безопасные и объектно-ориентированные.
Зачем нужны делегаты? Они позволяют передавать методы как параметры, создавать callback-функции, реализовывать паттерн «Наблюдатель» (Observer) и делать код более гибким.
Общая схема:
delegate возвращаемыйТип Имя(параметры);
Пример: delegate void MyDelegate(int x); — делегат, который может хранить метод, принимающий int и возвращающий void.
// 1. Объявляем делегат
delegate void MyDelegate(int x);
// 2. Создаём методы с той же сигнатурой
static void PrintDouble(int number)
{
Console.WriteLine(number * 2);
}
static void PrintSquare(int number)
{
Console.WriteLine(number * number);
}
// 3. Используем делегат
MyDelegate del = PrintDouble; // сохраняем ссылку на метод
del(5); // вызываем метод через делегат → 10
del = PrintSquare; // теперь ссылаемся на другой метод
del(5); // → 25
Делегат выступает как «посредник» между кодом, который хочет вызвать метод, и самим методом. Это пригодится для callback'ов, обработчиков событий и LINQ.
Объявление delegate void MyDelegate(int x)
Делегат объявляется с ключевым словом delegate. Сигнатура делегата должна полностью совпадать с сигнатурой метода, который он будет хранить. Это включает количество параметров, их типы и возвращаемый тип.
// Делегат без параметров, возвращает void
delegate void SimpleDelegate();
// Делегат с одним параметром, возвращает void
delegate void LogDelegate(string message);
// Делегат с двумя параметрами, возвращает int
delegate int MathDelegate(int a, int b);
// Делегат с параметром и возвращаемым bool
delegate bool FilterDelegate(int value);
// Использование
static void ShowTime() => Console.WriteLine(DateTime.Now);
static void Log(string msg) => Console.WriteLine($"[LOG] {msg}");
static int Add(int a, int b) => a + b;
static bool IsPositive(int v) => v > 0;
SimpleDelegate s = ShowTime;
s(); // → текущее время
LogDelegate log = Log;
log("Старт программы"); // → [LOG] Старт программы
MathDelegate math = Add;
Console.WriteLine(math(3, 7)); // → 10
FilterDelegate filter = IsPositive;
Console.WriteLine(filter(-5)); // → False
Multicast-делегаты: += и -=
Делегат может хранить не один, а несколько методов. Такой делегат называется multicast-делегатом. Методы добавляются через += и удаляются через -=. При вызове делегата все методы вызываются последовательно в порядке их добавления.
delegate void Notifier(string message);
static void SendEmail(string msg)
{
Console.WriteLine($"Email отправлен: {msg}");
}
static void SendSms(string msg)
{
Console.WriteLine($"SMS отправлено: {msg}");
}
static void LogToFile(string msg)
{
Console.WriteLine($"Записано в лог: {msg}");
}
Notifier notify = SendEmail;
notify += SendSms; // добавляем второй метод
notify += LogToFile; // добавляем третий метод
notify("Система запущена");
// Вывод:
// Email отправлен: Система запущена
// SMS отправлено: Система запущена
// Записано в лог: Система запущена
notify -= SendSms; // удаляем SMS-уведомление
notify("Обновление завершено");
// Вывод:
// Email отправлен: Обновление завершено
// Записано в лог: Обновление завершено
Notifier notify = null;
// notify("тест"); // NullReferenceException!
// Безопасный вызов:
if (notify != null)
{
notify("тест");
}
// Или через null-conditional (C# 6+):
notify?.Invoke("тест"); // ничего не произойдёт, ошибки нет
Встроенные делегаты: Func<>, Action<>, Predicate<>
В .NET есть три встроенных семейства делегатов, которые покрывают 99% случаев. Тебе не нужно объявлять собственные делегаты — используй готовые:
Action — это делегат, который ничего не возвращает (void). Может принимать до 16 параметров. Используется для действий, команд, callback'ов.
Action sayHello = () => Console.WriteLine("Привет!");
sayHello(); // → Привет!
Action<string> print = msg => Console.WriteLine(msg);
print("C# крутой!"); // → C# крутой!
Action<int, int> showSum = (a, b) =>
Console.WriteLine($"Сумма: {a + b}");
showSum(3, 5); // → Сумма: 8
Func — это делегат, возвращающий значение. Последний параметр в угловых скобках — это возвращаемый тип. Первые — типы входных параметров. Может принимать до 16 входных параметров.
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // → 25
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(5, 3)); // → 8
Func<double, double> celsiusToFahr =
c => c * 9 / 5 + 32;
Console.WriteLine(celsiusToFahr(0)); // → 32
Predicate<T> — это делегат, который принимает один параметр типа T и возвращает bool. Используется для проверки условий (фильтрации).
Predicate<int> isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // → True
Console.WriteLine(isEven(7)); // → False
List<int> numbers = new() { 1, 2, 3, 4, 5, 6 };
List<int> evens = numbers.FindAll(isEven);
Console.WriteLine(string.Join(", ", evens)); // → 2, 4, 6
Func<T1, T2, TResult> — последний тип всегда TResult (возвращаемое значение). Action<T1, T2> — все типы — параметры, возврат void. Predicate<T> — это фактически Func<T, bool>, но с более понятным названием.События: event keyword
Событие (event) — это специальная конструкция на основе делегата, которая позволяет объекту уведомлять другие объекты о том, что произошло некоторое действие. События — это основа GUI-программирования (нажатия кнопок, движение мыши) и паттерна «Издатель-Подписчик» (Publisher-Subscriber).
Ключевое отличие события от простого делегата: извне можно только += или -= обработчики, но нельзя перезаписать или вызвать событие напрямую.
Объявление и использование события:
public event DelegateType EventName;
// 1. Объявляем делегат для события
public delegate void ButtonClickHandler(string message);
// 2. Создаём класс с событием (издатель)
public class Button
{
// Объявляем событие
public event ButtonClickHandler OnClick;
public void Click(string userMessage)
{
Console.WriteLine($"Кнопка нажата: {userMessage}");
// Вызываем событие — уведомляем всех подписчиков
OnClick?.Invoke($"Кнопка нажата пользователем: {userMessage}");
}
}
// 3. Создаём подписчиков
public class Logger
{
public void Log(string msg)
{
Console.WriteLine($"[Логгер] {msg}");
}
}
public class EmailNotifier
{
public void SendNotification(string msg)
{
Console.WriteLine($"[Email] Отправлено уведомление: {msg}");
}
}
Теперь применим это в программе:
static void Main()
{
Button button = new Button();
Logger logger = new Logger();
EmailNotifier email = new EmailNotifier();
// Подписываемся на событие
button.OnClick += logger.Log;
button.OnClick += email.SendNotification;
// Вызываем событие
button.Click("Сохранить документ");
// Вывод:
// Кнопка нажата: Сохранить документ
// [Логгер] Кнопка нажата пользователем: Сохранить документ
// [Email] Отправлено уведомление: Кнопка нажата пользователем: Сохранить документ
// Отписываемся от события
button.OnClick -= email.SendNotification;
button.Click("Открыть файл");
// Вывод:
// Кнопка нажата: Открыть файл
// [Логгер] Кнопка нажата пользователем: Открыть файл
}
Подписка и вызов событий — детали
Чтобы правильно работать с событиями, нужно запомнить несколько важных правил:
Правила работы с событиями
- Событие может быть объявлено только внутри класса (или интерфейса).
- Извне к событию можно только подписаться (
+=) или отписаться (-=). - Вызвать событие (например,
button.OnClick("тест")) можно только из того же класса, где оно объявлено. - Событие нельзя присвоить напрямую:
button.OnClick = someMethod;— ошибка компиляции. - Перед вызовом события всегда проверяй его на null:
OnClick?.Invoke(msg). - Можно использовать встроенный делегат
EventHandlerиEventHandler<TEventArgs>.
// Стандартный делегат: (object sender, EventArgs e)
public event EventHandler? OnProcessCompleted;
// Вызов события (внутри класса)
protected virtual void OnProcessCompleted()
{
OnProcessCompleted?.Invoke(this, EventArgs.Empty);
}
// С собственными данными:
public class OrderEventArgs : EventArgs
{
public string OrderId { get; set; }
public decimal Amount { get; set; }
}
public event EventHandler<OrderEventArgs> OnOrderPlaced;
protected virtual void OnOrderPlaced(OrderEventArgs e)
{
OnOrderPlaced?.Invoke(this, e);
}
Практические примеры: UI-обработчики и callback'и
Делегаты и события повсеместно используются в реальных C#-приложениях. Вот самые частые сценарии:
Пример 1: Обработчики UI (например, Windows Forms)
// Windows Forms
Button saveButton = new Button();
saveButton.Text = "Сохранить";
saveButton.Click += (sender, e) =>
{
MessageBox.Show("Файл сохранён!");
};
// WPF
Button wpfButton = new Button();
wpfButton.Content = "Нажми меня";
wpfButton.Click += OnButtonClick;
private void OnButtonClick(object sender, RoutedEventArgs e)
{
// Обработка нажатия
}
Пример 2: Callback после завершения операции
void ProcessData(int[] data, Action<int> onItemProcessed)
{
foreach (var item in data)
{
int result = item * 2;
onItemProcessed(result); // вызываем callback
}
}
// Использование:
int[] numbers = { 1, 2, 3, 4, 5 };
ProcessData(numbers, result =>
{
Console.WriteLine($"Обработан элемент: {result}");
});
Пример 3: Фильтрация с помощью делегата (как в LINQ)
List<int> FilterList(List<int> list, Predicate<int> predicate)
{
List<int> result = new();
foreach (var item in list)
{
if (predicate(item))
result.Add(item);
}
return result;
}
List<int> numbers = new() { 1, 2, 3, 4, 5, 6, 7, 8 };
var evens = FilterList(numbers, x => x % 2 == 0);
var big = FilterList(numbers, x => x > 5);
Console.WriteLine(string.Join(", ", evens)); // → 2, 4, 6, 8
Console.WriteLine(string.Join(", ", big)); // → 6, 7, 8
Мини-тест
Вопрос 1: Что такое делегат в C#?
Правильный ответ: B
Вопрос 2: Какой синтаксис объявления делегата?
Правильный ответ: C
Вопрос 3: Какой встроенный делегат используется для методов, возвращающих значение?
Правильный ответ: B
Вопрос 4: Что делает оператор += с делегатом?
Правильный ответ: B
Вопрос 5: Чем событие отличается от делегата?
Правильный ответ: C
Задание
Система уведомлений о погоде
Создай консольное приложение, которое моделирует систему уведомлений:
- Объяви делегат
WeatherDelegate, принимающийstring city, int temperature. - Создай класс
WeatherStation(издатель) с событиемOnWeatherChanged. - Добавь метод
UpdateWeather(string city, int temp), который выводит информацию и вызывает событие. - Создай классы-подписчики:
PhoneApp(выводит уведомление на телефон),EmailService(отправляет email),Logger(пишет в лог). - В Main() подпиши все три подписчика на событие, вызови UpdateWeather для двух городов.
- Отпиши Logger и снова вызови UpdateWeather.
- Используй лямбда-выражение для подписки дополнительного уведомления.
Цель
Отработать объявление делегатов, событий, подписку и отписку, multicast-делегаты.
Сложность
Средняя
Что важно запомнить
delegate.+=, удаление через -=. Все методы вызываются последовательно.Action (void), Func<TResult> (возвращает значение), Predicate<T> (возвращает bool).OnClick?.Invoke(msg). Используй EventHandler для стандартных сценариев.Тест: 8.5: Делегаты и события
3 вопроса