JavaRush /Курсы /C# SELF /Примеры событийного программирования (

Примеры событийного программирования ( event)

C# SELF
54 уровень , 4 лекция
Открыта

1. Введение

Типичный новичок может спросить: «Где на практике применяются события? Неужели все взаимодействия между классами строятся на событиях, а не на прямых вызовах методов?» Ответ простой: события и делегаты — не волшебство, которое решает все проблемы архитектуры. Но без них современное приложение быстро превращается в «жёстко сцепленное» (tightly coupled), когда изменение одного компонента влечёт за собой необходимость менять множество других. Использование событий и делегатов помогает создавать гибкие, расширяемые и удобные для поддержки системы.

  • UI-программирование (WinForms, WPF, Xamarin, MAUI): обработка кликов, наведения, ввода текста и других пользовательских действий.
  • Асинхронные операции: завершение загрузки файла, получение данных из сети, срабатывание таймеров.
  • Архитектура плагинов и расширяемых систем: позволяет подключать новые модули без жёсткой интеграции в основной код.
  • Сигнальные/рассылающие системы: оповещение множества заинтересованных компонентов о произошедших событиях.
  • Наблюдение за изменениями состояния («наблюдатель»): реагирование на появление нового сообщения, изменение данных, обновление интерфейса.

А теперь — к делу!

2. Архитектурный скелет

Давайте представим, что вы разрабатываете маленькое консольное приложение — "Мини-чат" для внутреннего обучения в компании (или просто для прокачки навыков). У нас есть сущности: Пользователь, Чат и, возможно, Бот-помощник. Когда пользователь отправляет сообщение, чат должен оповестить всех подключённых пользователей и ботов — чтобы они вывели сообщение на экран или, в случае бота, сгенерировали автоответ. Это классический сценарий: издатель генерирует событие (event), обработчики подписаны на делегат EventHandler/EventHandler<TEventArgs>, а метод-обработчик вроде OnMessageReceived реагирует на уведомление.


// Класс-пользователь (подписчик)
public class User
{
    public string Name { get; }

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

    // Метод, который будет обработчиком события
    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        Console.WriteLine($"[{Name}] увидел новое сообщение: {e.MessageText}");
    }
}

// Аргументы события
public class MessageEventArgs : EventArgs
{
    public string MessageText { get; }

    public MessageEventArgs(string text) => MessageText = text;
}

// Класс-чат (издатель события)
public class ChatRoom
{
    public event EventHandler<MessageEventArgs>? MessageReceived;

    public void SendMessage(string text)
    {
        // Генерируем событие (оповещаем всех подписчиков)
        MessageReceived?.Invoke(this, new MessageEventArgs(text));
    }
}

Пример использования:

var chat = new ChatRoom();
var user1 = new User("Антон");
var user2 = new User("Мария");

chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;

chat.SendMessage("Привет, всем! 😊");

В консоли увидим два сообщения — оба пользователя узнали о новом сообщении.

3. Динамическая подписка и отписка

В реальной жизни пользователь может выйти из чата и больше не желать получать сообщения. Давайте научим пользователя отписываться корректно (оператор -=):

// В классе User можно добавить метод "Выйти из чата"
public void Unsubscribe(ChatRoom chat)
{
    chat.MessageReceived -= OnMessageReceived;
}

Развиваем пример:

var chat = new ChatRoom();
var user1 = new User("Антон");
var user2 = new User("Мария");

chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;

chat.SendMessage("Первая новость");

user2.Unsubscribe(chat); // Мария покидает чат

chat.SendMessage("Мария больше не увидит это сообщение");

Динамическая подписка/отписка встречается везде: окна закрываются, вкладки закрываются, временные сервисы отписываются от глобальных источников событий. Не забывайте — неотписавшийся подписчик превращается в зомби, а приложение — в утекатель памяти!

4. Обработка сообщений и генерация ответа

Теперь добавим бота, который реагирует на каждое сообщение. Пусть бот говорит "Привет!" автоматически, если в сообщении есть слово "бот". Заодно рассмотрим множественную подписку и демонстрацию многоадресных делегатов.

public class Bot
{
    public string Name { get; }

    public Bot(string name) => Name = name;

    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        // Бот реагирует на ключевое слово
        if (e.MessageText.Contains("бот", StringComparison.OrdinalIgnoreCase))
        {
            if (sender is ChatRoom chatRoom)
            {
                Console.WriteLine($"[Бот {Name}]: Здравствуйте! Чем могу помочь?");
                // Бот отправляет сообщение в ответ
                chatRoom.SendMessage($"Бот {Name} готов вам помочь.");
            }
        }
    }
}

Используем:

var chat = new ChatRoom();
var user = new User("Евгений");
var bot = new Bot("Помощник");

chat.MessageReceived += user.OnMessageReceived;
chat.MessageReceived += bot.OnMessageReceived;

// Евгений пишет сообщение, которое триггерит бота
chat.SendMessage("Привет, бот, как дела?");

Консоль выдаст (демонстрируем порядок вызова — он не гарантируется!):

[Евгений] увидел новое сообщение: Привет, бот, как дела?
[Бот Помощник]: Здравствуйте! Чем могу помочь?
[Евгений] увидел новое сообщение: Бот Помощник готов вам помочь.
[Бот Помощник]: Здравствуйте! Чем могу помочь?
[Евгений] увидел новое сообщение: Бот Помощник готов вам помочь.
[Бот Помощник]: Здравствуйте! Чем могу помочь?
...

Кто заметил потенциальный баг? Правильно, бесконечный цикл: бот реагирует на свои же сообщения (он снова видит слово "бот"). Один из способов предотвращения этого — добавить простую проверку:

public void OnMessageReceived(object? sender, MessageEventArgs e)
{
    // Бот не реагирует на свои собственные сообщения
    if (e.MessageText.Contains("бот", StringComparison.OrdinalIgnoreCase) &&
        !e.MessageText.Contains(Name))
    {
        if (sender is ChatRoom chatRoom)
        {
            Console.WriteLine($"[Бот {Name}]: Здравствуйте! Чем могу помочь?");
            chatRoom.SendMessage($"Бот {Name} готов вам помочь.");
        }
    }
}

На практике такие ситуации — отличный повод подумать о обработке циклических событий, лямбда-выражениях или даже механизме отмены дальнейших обработчиков (см. предыдущие лекции).

5. Асинхронные операции и колбэки

Очень часто события используются для оповещения о завершении асинхронной операции. Например, загрузка данных из интернета или длительный расчёт: событие Completed сообщает о конце работы, а метод RunLongOperationAsync выполняет длительную операцию.

public class LongRunner
{
    // Событие о завершении работы
    public event EventHandler<EventArgs>? Completed;

    public async Task RunLongOperationAsync()
    {
        Console.WriteLine("Долгая операция началась...");
        await Task.Delay(2000); // Это имитация долгой работы
        Console.WriteLine("Операция завершена, оповещаем подписчиков.");
        Completed?.Invoke(this, EventArgs.Empty);
    }
}

Клиентский код:

var runner = new LongRunner();

// Подписка на событие завершения
runner.Completed += (sender, e) =>
{
    Console.WriteLine("Получено уведомление: операция завершена!");
};

await runner.RunLongOperationAsync();

Это фундамент асинхронного взаимодействия — события часто используются в UI-фреймворках, чтобы сигнализировать о завершении загрузки, окончания анимации, кликах пользователя и т.д.

6. Сигнальные системы

Рассмотрим другую классическую задачу: есть система уведомлений (например, в интернет-магазине, когда скидка на товар). Все заинтересованные "слушатели" узнают об этом по событию SaleOccurred:

public class SaleNotifier
{
    public event EventHandler<SaleEventArgs>? SaleOccurred;

    public void AnnounceSale(string product, decimal newPrice)
    {
        SaleOccurred?.Invoke(this, new SaleEventArgs(product, newPrice));
    }
}

public class SaleEventArgs : EventArgs
{
    public string Product { get; }
    public decimal NewPrice { get; }
    public SaleEventArgs(string product, decimal price)
    {
        Product = product; NewPrice = price;
    }
}

public class Customer
{
    public string Name { get; }
    public Customer(string name) => Name = name;

    public void OnSale(object? sender, SaleEventArgs e)
    {
        Console.WriteLine($"[{Name}] получил уведомление: {e.Product} теперь стоит {e.NewPrice.ToString("F2")} у.е.!");
    }
}

Используем:

var notifier = new SaleNotifier();
var c1 = new Customer("Андрей");
var c2 = new Customer("Ольга");

notifier.SaleOccurred += c1.OnSale;
notifier.SaleOccurred += c2.OnSale;

notifier.AnnounceSale("Чайник", 999.99m);

Если один из клиентов больше не заинтересован — отписываем:

notifier.SaleOccurred -= c2.OnSale;
notifier.AnnounceSale("Миксер", 1999.99m);

Такой паттерн используется для рассылки сигналов (notification, pub/sub), что очень важно в современных архитектурах.

7. Реализация механизма отмены цепочки событий

Иногда один из обработчиков должен "остановить" дальнейшее оповещение. Обычно для этого используют наследника EventArgs с флагом отмены. В примере ниже мы вручную перебираем GetInvocationList() и прекращаем выполнение при Cancel = true.

public class CancelEventArgs : EventArgs
{
    public bool Cancel { get; set; }
}

public class EventSource
{
    public event EventHandler<CancelEventArgs>? SomethingHappened;

    public void DoSomething()
    {
        var args = new CancelEventArgs();
        // Классический цикл для перебора подписчиков (если нужен контроль над порядком)
        var handlers = SomethingHappened?.GetInvocationList();
        if (handlers != null)
        {
            foreach (var handler in handlers)
            {
                ((EventHandler<CancelEventArgs>)handler)(this, args);
                if (args.Cancel)
                {
                    Console.WriteLine("Цепочка сообщений прервана.");
                    break;
                }
            }
        }
    }
}

Используем:

var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Первый обработчик");
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Второй обработчик отменяет событие.");
    e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Этот обработчик не будет вызван.");
};

source.DoSomething();

Результат:

Первый обработчик
Второй обработчик отменяет событие.
Цепочка сообщений прервана.

Такой механизм нужен валидации, обработке событий перед закрытием окна, проверке разрешений и других задачах, где важна возможность сказать "Стоп! Дальше не обрабатываем."

8. Потокобезопасность и управление подписчиками

В многопоточных приложениях, где подписчики могут добавляться и удаляться одновременно с генерацией события, важно использовать потокобезопасные шаблоны (см. официальная документация). Для контроля можно реализовать событие вручную с аксессорами add и remove и защищать доступ через lock:

public class CustomEvent
{
    private EventHandler? _handlers;

    public event EventHandler SomethingHappened
    {
        add
        {
            lock (this) // Потокобезопасно
            {
                _handlers += value;
            }
        }
        remove
        {
            lock (this)
            {
                _handlers -= value;
            }
        }
    }

    protected void RaiseEvent()
    {
        // Используем копию
        EventHandler? handler;
        lock (this)
        {
            handler = _handlers;
        }
        handler?.Invoke(this, EventArgs.Empty);
    }
}

Этот подход встречается редко (обычно стандартного механизма достаточно), но иногда нужен для высоконагруженных систем.

9. События в UI — из WinForms/WPF/MAUI

WinForms:

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("Кнопка нажата!");
}

За этим методом скрывается механизм подписки:

button1.Click += button1_Click;

WPF/MAUI: события типа PropertyChanged (уведомления об изменении свойств модели):

public class MyViewModel : INotifyPropertyChanged
{
    private string _value;
    public string Value
    {
        get => _value;
        set
        {
            if (_value != value)
            {
                _value = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

Фреймворки активно используют события для построения реактивных интерфейсов (MVVM).

10. Типичные ошибки и грабли при работе с событиями

Ошибка №1: создание утечек памяти.
Это самая частая проблема. Если короткоживущий объект (например, окно или временный сервис) подписывается на событие долгоживущего объекта (например, глобального кэша или статического класса) и не отписывается при своём уничтожении, он навсегда останется в памяти. Всегда реализуйте отписку в методе Dispose или в другом подходящем месте жизненного цикла объекта.

Ошибка №2: создание бесконечных циклов событий.
Подписчик может отреагировать на событие, вызвав действие, которое снова генерирует то же самое событие. Это приводит к бесконечной рекурсии и падению приложения с StackOverflowException. Всегда проверяйте условия, чтобы обработчик не реагировал на события, которые он же и спровоцировал.

Ошибка №3: зависимость от порядка вызова подписчиков.
Никогда не полагайтесь на то, что обработчики событий будут вызваны в том же порядке, в котором они были подписаны. Спецификация C# и реализация CLR этого не гарантируют. Если порядок важен, вызывайте подписчиков вручную через GetInvocationList() в нужной последовательности.

Ошибка №4: небезопасная работа с событиями в многопоточной среде.
Если подписчики добавляются или удаляются из одного потока, а событие вызывается из другого, может возникнуть состояние гонки. Классическая защита — копировать делегат в локальную переменную перед вызовом:

var handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);

Или использовать lock при ручной реализации событий через add/remove.

2
Задача
C# SELF, 54 уровень, 4 лекция
Недоступна
Система уведомлений о распродажах
Система уведомлений о распродажах
1
Опрос
Типовые ошибки с делегатами, 54 уровень, 4 лекция
Недоступен
Типовые ошибки с делегатами
Лучшие практики событийного программирования
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ