Форум программистов, компьютерный форум, киберфорум
stackOverflow
Войти
Регистрация
Восстановить пароль

Делегаты и события C# в лучших практиках Event-Driven программирования

Запись от stackOverflow размещена 08.05.2025 в 15:10
Показов 2233 Комментарии 0

Нажмите на изображение для увеличения
Название: 154184c3-7320-4cd1-90e9-ebfbfa0602b9.jpg
Просмотров: 57
Размер:	167.0 Кб
ID:	10767
Помню, как в 2010-м работал над своим первым крупным проектом на C#. Это была система обработки торговых ордеров, и мой код напоминал спагетти из проверок условий. «Если пришёл запрос — обработай и оповести все зависимые модули». На каждое действие я создавал десятки явных вызовов методов различных классов. Хрупкая конструкция разваливалась при малейшем изменении требований. А потом я открыл для себя делегаты и событий. Эти две концепции радикально трансформировали мой подход к архитектуре. Делегаты — по сути, тип-сейф указатели на функции — позволили передавать поведение как данные. События же, построеные на основе делегатов, создали механизм уведомлений, где объект-издатель даже не знает, кто именно его слушает.

Делегаты в C# — это больше чем просто callback-функции. Это полноценные объекты первого класса, интегрированные в систему типов языка. События — специализированные делегаты с защитой от неправильного использования. Вместе они формируют мощный инструментарий для построения слабосвязанных, гибких и масштабируемых систем.

Фундаментальные основы делегатов в C#



В самой сути делегат — это безопасный по типам указатель на метод. В отличии от указателей функции из C/C++, делегаты в C# полноценные объекты, интегрированные в объектную модель языка, а значит к ним применимы все операции, что и к обычным объектам. Формальное объявление делегата выглядит так:

C#
1
public delegate int CalculationOperation(int firstValue, int secondValue);
Что здесь происходит? Мы определяем новый *тип* — CalculationOperation, который указывает на методы с определённой сигнатурой: принимающие два целых и возвращающие целое. Представьте его как контракт: «любой метод с такими входами и выходом может быть присвоен делегату этого типа». Создание и использование делегата выглядит так:

C#
1
2
3
4
public static int Add(int x, int y) => x + y;
 
CalculationOperation calc = Add;
int result = calc(5, 10); // result = 15
Красота делегатов в их гибкости. Мы можем хранить методы, передавать их как параметры, возвращать из функций — в общем, обращаться с ними как с данными.

Типы делегатов



В C# существует несколько вариаций делегатов:

1. Пользовательские делегаты. Те, что мы объявляем сами, как CalculationOperation выше.
2. Встроенные делегаты из .NET:
- Action<T1, T2, ...> — для методов без возвращаемого значения.
C#
1
2
   Action<string> logMessage = msg => Console.WriteLine($"LOG: {msg}");
   logMessage("Приложение запущено");
- Func<T1, T2, ..., TResult> — для методов, возвращающих значение.
C#
1
2
   Func<double, double> square = x => x * x;
   Console.WriteLine(square(4.5)); // 20.25
- Predicate<T> — специализированный делегат для методов, возвращающих bool.
C#
1
2
   Predicate<int> isPositive = n => n > 0;
   Console.WriteLine(isPositive(-5)); // false

Делегаты и функциональное программирование



С появлением LINQ и лямбда-выражений C# начал активно двигаться в сторону функционального программирования. Делегаты — один из ключевых элементов, который сделал это возможным.
Вот красивый пример, демонстрирующий функции высшего порядка — функции, работающие с другими функциями:

C#
1
2
3
4
5
6
7
Func<Func<int, int>, Func<int, int>> compose = f => g => x => f(g(x));
 
Func<int, int> addFive = x => x + 5;
Func<int, int> multiplyByThree = x => x * 3;
 
var addAndMultiply = compose(multiplyByThree)(addFive);
Console.WriteLine(addAndMultiply(10)); // (10 + 5) * 3 = 45
Данный пример демонстрирует композицию функций, что является основным принципом функцинального программирования. Делегат compose принимает две функции и создаёт новую функцию, которая применяет их последовательно.

Многоадресные делегаты



Пожалуй, самой крутой особенностью делегатов является их способность указывать на несколько методов одновременно. Такие делегаты называются многоадресными.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public delegate void Notification(string message);
 
static void EmailAlert(string msg) => Console.WriteLine($"EMAIL: {msg}");
static void SmsAlert(string msg) => Console.WriteLine($"SMS: {msg}");
static void PushAlert(string msg) => Console.WriteLine($"PUSH: {msg}");
 
// Создаём и комбинируем делегат
Notification notify = EmailAlert;
notify += SmsAlert;
notify += PushAlert;
 
// Один вызов - три метода
notify("Сервер перезагружен");
При вызове многоадресного делегата, все связанные методы вызываются последовательно. Это отлично работает с делегатами типа void, но имеет особености при возвращении значений: возвращается только результат последнего метода.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Func<int, int> transformations = x => {
    Console.WriteLine($"Добавляем 1: {x} -> {x+1}");
    return x + 1;
};
 
transformations += x => {
    Console.WriteLine($"Умножаем на 2: {x} -> {x*2}");
    return x * 2;
};
 
int result = transformations(5);
// Выведет:
// Добавляем 1: 5 -> 6
// Умножаем на 2: 5 -> 10
// result = 10 (только результат последнего метода)
Интересно, что при ошибке в одном из методов цепочки, выполнение прерывается, и последующие методы не выполняются. Помню, как эта особенность привела к трудноуловимому багу в одном из моих проектов.
Операторы += и -= используются для добавления и удаления методов соответственно:

C#
1
notify -= SmsAlert; // удаляем один из обработчиков
Многоадресные делегаты особено полезны при реализации механизма событий, о которых мы поговорим в следующей части. Именно они позволяют нескольким объектам реагировать на одно событие независимо друг от друга.

Анонимные методы и лямбда-выражения



Хотя синтаксис с явным объявлением делегатов довольно чёткий и понятный, со временем он становится немного многословным. Представьте, что вам необходимо создать простой обработчик события для кнопки, который выводит сообщение. Классический подход потребовал бы объявления отдельного метода где-то в классе. К счастью, C# 2.0 внедрил концепцию анонимных методов, а C# 3.0 усовершенствовал её с появлением лямбда-выражений. Это просто революция в лаконичности кода!

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// Старый способ (C# 1.0)
button.Click += new EventHandler(Button_Click);
void Button_Click(object sender, EventArgs e) { 
    Console.WriteLine("Кнопка нажата"); 
}
 
// Анонимный метод (C# 2.0)
button.Click += delegate(object sender, EventArgs e) { 
    Console.WriteLine("Кнопка нажата"); 
};
 
// Лямбда-выражение (C# 3.0+)
button.Click += (sender, e) => Console.WriteLine("Кнопка нажата");
Вы только посмотрите на этот прогресс! Три строчки плюс отдельный метод превратились в одну элегантную строку. Лямбды — не просто синтаксический сахар, они существенно влияют на стиль программирования и читаемость кода.
Есть два типа лямбда-выражений:

C#
1
2
3
4
5
6
7
8
9
10
11
// Лямбда-выражение (expression lambda)
Func<int, int, int> add = (x, y) => x + y;
 
// Лямбда-оператор (statement lambda)
Func<int, int, int> complexCalculation = (x, y) => {
    int result = 0;
    for (int i = 0; i < y; i++) {
        result += x;
    }
    return result;
};
Каждое лямбда-выражение генерирует метод во время компиляции. Компилятор создаёт этот метод и подставляет его в соответствующий делегат. Забавно, но иногда я вижу в декомпиляторах, как мои изящные однострочные лямбды превращаются в многострочные функции с замысловатыми именами.

Захват переменных и замыкания



Одна из наиболее мощных (и порой коварных) возможностей лямбда-выражений — захват переменных из окружающего контекста. Такой механизм называется замыканием (closure).

C#
1
2
3
4
5
6
int counter = 0;
Func<int> getNextNumber = () => ++counter;
 
Console.WriteLine(getNextNumber()); // 1
Console.WriteLine(getNextNumber()); // 2
Console.WriteLine(counter);         // 2
Здесь лямбда-выражение () => ++counter захватывает переменную counter из внешнего контекста и может её изменять. Я помню, как это свойство неявно создало потенциальную утечку памяти в моём коде. Подписка на событие захватила объекты, которые должны были быть утилизированы, но не могли из-за этой ссылки. Поэтому будьте осторожны с захватом контекста — особенно в долгоживущих делегатах.

Ковариантность и контравариантность делегатов



Ковариантность и контравариантность — это два термина, от которых у многих разработчиков мурашки по коже. Но эти концепции не так страшны, как звучит, и в контексте делегатов они вполне понятны. Представьте иерархию классов:

C#
1
2
class Animal { }
class Dog : Animal { }
С C# 2.0 и выше, вы можете выполнить следующие присвоения:

C#
1
2
3
4
5
6
// Ковариантность - возвращаемый тип может быть более конкретным
Func<Animal> getAnimal = () => new Dog(); // Возвращаем Dog вместо Animal
 
// Контравариантность - параметр может быть более общим
Action<Dog> processDog = dog => { /* что-то делаем */ };
Action<Animal> processAnimal = processDog; // Требует Animal, но принимает Dog
Эти возможности делегатов расширяют области их применения и помогают создавать более гибкие и переиспользуемые компоненты.

Делегаты и обобщения (Generics)



С появлением обобщённых типов в C# 2.0, делегаты получили новое измерение гибкости. Теперь можно было создавать типизированные делегаты, работающие с разными типами данных:

C#
1
2
3
4
5
6
// Обобщённый делегат для трансформации данных
public delegate TResult Transformer<T, TResult>(T input);
 
// Использование
Transformer<string, int> getLength = s => s.Length;
Console.WriteLine(getLength("Hello, world!")); // 13
Такой подход позволяет создавать универсальные алгоритмы обработки данных, не привязываясь к конкретным типам. Я часто применяю этот приём в утилитарных классах, которые должны работать с разнообразными данными. В сочетании с ковариантностью и контравариантностью, обобщённые делегаты становятся действительно мощным инструментом, хотя требуют некоторого времени для освоения.

Практическая ценность делегатов



За годы работы с C# я нашёл несколько шаблонов использования делегатов, которые стабильно оказываются полезными:

1. Стратегия выполнения - когда алгоритм должен меняться в зависимости от контекста.

C#
1
2
3
4
5
6
7
8
9
10
Dictionary<string, Func<int, int, int>> operations = new() {
    { "add", (a, b) => a + b },
    { "subtract", (a, b) => a - b },
    { "multiply", (a, b) => a * b },
    { "divide", (a, b) => b != 0 ? a / b : throw new DivideByZeroException() }
};
 
// Использование
string operation = "multiply";
int result = operations[operation](10, 5); // 50
2. Колбэки и асинхронная обработка - делегаты идеальны для уведомлений о завершении асинхронных операций.
3. Обработка коллекций - LINQ полностью построен на делегатах, что делает обработку коллекций кристально ясной.

Для чего использовать ключевое слово event в объявлении события, если события — это те же самые делегаты
Господа, скажите пожалуйста, для чего использовать ключевое слово event в объявлении события, если...

Data Driven Test, провайдер базы данных
Добрый день! Пытаюсь настроить в VS 2010 тестирование. Не могу понять какой провайдер нужно...

Data driven test по данным из Access
вот есть такой тестusing System; using System.Collections.Generic; using System.Linq; using...

Как правильно приготовить DDD (domain-driven design)
Достался проект, который изначально планировался как DDD, но ребята которые его делали до меня...


События как особый механизм



События в C# — это своеобразная надстройка над делегатами, но с существенными отличиями. Если делегат — это тип-сейф указатель на метод, то событие — это специализированный делегат с ограниченным доступом. Помню свои первые эксперименты с событиями. Я всё никак не мог понять, зачем нужна эта обёртка, если есть делегаты? Со временем пришло понимание — события решают проблему инкапсуляции, которую чистые делегаты не решают. Они дают возможность внешнему коду подписываться или отписываться от уведомлений, но не позволяют напрямую вызывать делегат или присваивать ему значение.

Отличия от стандартных делегатов



Главное отличие событий от обычных делегатов можно описать в терминах контракта между классами:
1. Ограниченный интерфейс — внешний код может использовать только операторы += и -= для подписки/отписки.
2. Инкапсуляция вызова — только класс, объявивший событие, может его вызвать.
3. Защита от полной перезаписи — невозможно "перехватить" чужих подписчиков, заменив делегат новым.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Publisher {
    // Событие - объявляется с ключевым словом event
    public event EventHandler SomethingHappened;
 
    public void DoSomething() {
        // Вызов события (только внутри класса)
        SomethingHappened?.Invoke(this, EventArgs.Empty);
    }
}
 
public class Subscriber {
    public void RegisterWithPublisher(Publisher publisher) {
        // Подписка на событие (извне доступны только += и -=)
        publisher.SomethingHappened += OnSomethingHappened;
    }
 
    private void OnSomethingHappened(object sender, EventArgs e) {
        Console.WriteLine("Получено событие!");
    }
}
Этот код демонстрирует ключевые отличия событий от делегатов. Попытка вызвать событие извне класса (publisher.SomethingHappened(this, EventArgs.Empty)) или присвоить ему значение (publisher.SomethingHappened = null) приведёт к ошибке компиляции.

Паттерн "издатель-подписчик" (Observer)



События — идеальный механизм реализации паттерна "издатель-подписчик" (Observer). В этом паттерне есть два ключевых участника:
Издатель (Publisher) — объект, который генерирует события,
Подписчик (Subscriber) — объект, который реагирует на события.

Самое главное преимущество этого паттерна — слабая связанность компонентов. Издатель ничего не знает о своих подписчиках, их может быть сколько угодно, и они могут динамически добавляться и удаляться. Издатель просто сообщает: "Эй, произошло событие X" — и все заинтересованные стороны реагируют по-своему. Например, изменение значения в текстовом поле может вызвать: обновление данных в модели, перерисовку графика, сохранение черновика, проверку валидности и т.д. Причём все эти действия происходят независимо друг от друга, и добавление новой функциональности не требует изменения существующего кода.

EventHandler и EventArgs



В .NET есть стандартный делегат для событий — EventHandler:

C#
1
2
public delegate void EventHandler(object sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Параметр sender указывает на объект, который вызвал событие, а e содержит дополнительные данные о событии. Базовый класс EventArgs не содержит никаких данных, но вы можете создавать наследников для передачи контекстной информации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PriceChangedEventArgs : EventArgs {
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
 
    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) {
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}
 
public class Product {
    private decimal _price;
    
    public event EventHandler<PriceChangedEventArgs> PriceChanged;
    
    public decimal Price {
        get => _price;
        set {
            if (_price == value) return;
            
            decimal oldPrice = _price;
            _price = value;
            
            // Вызываем событие с данными об изменении
            PriceChanged?.Invoke(this, new PriceChangedEventArgs(oldPrice, _price));
        }
    }
}
Такой подход даёт подписчикам богатую контекстную информацию о событии, позволяя принимать более информированные решения о реакции.

Синтаксические особенности объявления событий



Событие можно объявить несколькими способами:

1. Стандартное объявление — наиболее распространённый вариант
C#
1
public event EventHandler<MouseEventArgs> MouseMove;
2. Явная реализация с полями доступа — для более тонкого контроля
C#
1
2
3
4
5
6
private EventHandler<MouseEventArgs> _mouseMove;
 
public event EventHandler<MouseEventArgs> MouseMove {
    add { _mouseMove += value; Console.WriteLine("Добавлен обработчик"); }
    remove { _mouseMove -= value; Console.WriteLine("Удалён обработчик"); }
}
3. Статические события — для событий на уровне типа, не экземпляра
C#
1
public static event EventHandler<AppDomainInitializedEventArgs> DomainInitialized;
Особенно интересны события с пользовательскими акцессорами (add/remove), они позволяют добавить логику при подписке/отписке. Я использовал этот механизм для реализации пула событий, чтобы снизить потребление памяти в одном высоконагруженном приложении.

Вызов событий



Вызов события требует некоторой осторожности. Во-первых, нужно проверить событие на null, так как если у события нет подписчиков, то оно будет null. Во-вторых, желательно создать защищённый метод для вызова события:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Button {
    public event EventHandler Clicked;
    
    protected virtual void OnClicked() {
        // Проверка на null с использованием оператора ?. (C# 6.0+)
        Clicked?.Invoke(this, EventArgs.Empty);
        
        // Альтернативный подход (до C# 6.0)
        // var handler = Clicked;
        // if (handler != null) handler(this, EventArgs.Empty);
    }
    
    public void SimulateClick() {
        // Какая-то логика...
        OnClicked();
    }
}
Создание метода OnXXX для вызова события — это общепринятый паттерн в .NET. Он не только упрощает вызов события, но и позволяет переопределять логику вызова в производных классах.

События в многопоточных приложениях



Когда я столкнулся с первыми проблемами в многопоточных приложениях с использованием событий, у меня сложилось твёрдое убеждение: «события и многопоточность — как огонь и бензин, надо очень-очень осторожно». Основная проблема заключается в том, что подписка и вызов события могут происходить из разных потоков одновременно. Представьте сценарий: один поток активно вызывает событие, а другой поток в это время отписывается от него. Это может привести к исключению NullReferenceException или другим непредсказуемым результатам.

C#
1
2
3
4
5
6
7
8
9
10
11
12
// Проблемный код (не используйте его в реальных проектах!)
public void InvokeEvent()
{
    // Поток 1 вызывает событие
    MyEvent(this, EventArgs.Empty);  // Может вызвать NullReferenceException
}
 
public void UnsubscribeFromEvent()
{
    // Поток 2 отписывается от события
    MyEvent -= MyEventHandler;
}
Стандартным решением этой проблемы является локальное копирование ссылки на делегат перед его вызовом:

C#
1
2
3
4
5
6
7
8
9
10
11
public void InvokeEventSafely()
{
    // Создаём локальную копию делегата
    var handler = MyEvent;
    
    // Проверяем на null и вызываем безопасно
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}
Это работает, потому что даже если другой поток отпишется от события после создания локальной копии, мы все ровно используем старую версию делегата, содержащую все методы на момент копирования. Синтаксис с C# 6.0 делает этот код еще более элегантным:

C#
1
MyEvent?.Invoke(this, EventArgs.Empty);
Но учтите, что ?. оператор тоже создаёт локальную копию, что делает его безопасным в многопоточной среде.
Иногда требуется более жесткая синхронизация. Я работал над проектом, где требовалось обеспечить, чтобы события всегда вызывались из определённого потока (обычно UI-потока в GUI-приложениях). В таких случаях полезно использовать диспетчеризацию:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// В WPF приложении
Application.Current.Dispatcher.Invoke(() => {
    MyEvent?.Invoke(this, EventArgs.Empty);
});
 
// В WinForms
if (control.InvokeRequired)
{
    control.Invoke(new Action(() => MyEvent?.Invoke(this, EventArgs.Empty)));
}
else
{
    MyEvent?.Invoke(this, EventArgs.Empty);
}

Слабые события и предотвращение утечек памяти



Теперь о ещё одной засаде событийно-ориентированного программирования — утечках памяти. Самая распротраненная причина их возникновения — это забытые подписки на события долгоживущих объектов.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class EventPublisher
{
    public event EventHandler SomeEvent;
    
    public void RaiseEvent()
    {
        SomeEvent?.Invoke(this, EventArgs.Empty);
    }
}
 
public class ShortLivedSubscriber
{
    private readonly EventPublisher _publisher;
    
    public ShortLivedSubscriber(EventPublisher publisher)
    {
        _publisher = publisher;
        _publisher.SomeEvent += OnSomeEvent;  // Подписка на событие
    }
    
    private void OnSomeEvent(object sender, EventArgs e)
    {
        Console.WriteLine("Событие получено!");
    }
    
    // Забыли отписаться от события при уничтожении объекта!
}
В этом примере, даже если объект ShortLivedSubscriber больше не используется и должен быть собран сборщиком мусора, этого не произойдёт. Почему? Потому что EventPublisher все ещё хранит ссылку на метод OnSomeEvent, который принадлежит объекту ShortLivedSubscriber. Это создаёт циклическую ссылку, которую сборщик мусора не может разорвать. Для решения этой проблемы есть несколько подходов:

1. Явное отписывание от событий
C#
1
2
3
4
public void Dispose()
{
    _publisher.SomeEvent -= OnSomeEvent;
}
2. Слабые ссылки (Weak References)
Слабые ссылки позволяют сборщику мусора собирать объект, даже если на него есть ссылка. В .NET Framework есть WeakEventManager для WPF, а в .NET Standard можно использовть сторонние библиотеки или создать собственную реализацию. Вот упрощенный пример слабого события:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class WeakEventManager<TEventArgs> where TEventArgs : EventArgs
{
    private readonly Dictionary<WeakReference, Action<object, TEventArgs>> _handlers = 
        new Dictionary<WeakReference, Action<object, TEventArgs>>();
    
    public void AddHandler(object subscriber, Action<object, TEventArgs> handler)
    {
        _handlers.Add(new WeakReference(subscriber), handler);
    }
    
    public void RemoveHandler(object subscriber)
    {
        var keyToRemove = _handlers.Keys.FirstOrDefault(wr => wr.Target == subscriber);
        if (keyToRemove != null)
        {
            _handlers.Remove(keyToRemove);
        }
    }
    
    public void RaiseEvent(object sender, TEventArgs args)
    {
        foreach (var pair in _handlers.ToList())
        {
            if (pair.Key.Target is object target)
            {
                pair.Value(sender, args);
            }
            else
            {
                // Объект больше не существует, удаляем его обработчик
                _handlers.Remove(pair.Key);
            }
        }
    }
}
Современные фреймворки часто предоставляют готовые решения для слабых событий. Например, в ReactiveUI есть методы, которые подписывают на события с использованием слабых ссылок автоматически.

3. Использование шаблонов, которые обеспечивают автоматическую отписку
Различные фреймворки и библиотеки предлагают альтернативные подходы, которые решают проблему утечек иначе. Например, Rx.NET (Reactive Extensions) использует паттерн на основе IDisposable:

C#
1
2
3
4
5
6
7
8
// Использование Rx.NET
IDisposable subscription = Observable.FromEventPattern<EventArgs>(
    h => source.SomeEvent += h,
    h => source.SomeEvent -= h)
    .Subscribe(pattern => Console.WriteLine("Событие получено!"));
 
// Отписка просто вызывает Dispose
subscription.Dispose();
Я обнаружил, что комбинация всех этих методов наиболее эффективна в крупных проектах. Иногда используешь явное отписывание, иногда — слабые ссылки, а иногда — реактивные шаблоны. Выбор зависит от конкретного сценария и жизненного цикла объектов.

Практическое применение



Реализация паттерна "Стратегия" с делегатами



Паттерн "Стратегия" - один из классических паттернов проектирования, который позволяет менять поведение объекта во время выполнения программы. Традиционная реализация требует создания интерфейса и множества классов-стратегий. Но с делегатами всё становится намного проще:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TextProcessor
{
    private Func<string, string> _processingStrategy;
 
    public void SetStrategy(Func<string, string> strategy)
    {
        _processingStrategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
    }
 
    public string ProcessText(string input)
    {
        return _processingStrategy != null 
            ? _processingStrategy(input) 
            : throw new InvalidOperationException("Стратегия обработки не установлена");
    }
}
 
// Применение
var processor = new TextProcessor();
 
// Устанавливаем разные стратегии с помощью лямбд
processor.SetStrategy(s => s.ToUpper()); // Стратегия превращения в верхний регистр
Console.WriteLine(processor.ProcessText("привет, мир")); // ПРИВЕТ, МИР
 
processor.SetStrategy(s => new string(s.Reverse().ToArray())); // Стратегия переворота
Console.WriteLine(processor.ProcessText("привет, мир")); // рим ,тевирп
 
processor.SetStrategy(s => string.Join(" ", s.Split(' ').Select(word => word.Length.ToString()))); // Длина слов
Console.WriteLine(processor.ProcessText("привет, мир")); // 7 3
Вместо иерархии из инерфейса и реализующих его классов - всего несколько строчек кода. Элегантно, не правда ли? Такая реализация особенно полезна, когда стратегии просты и их реализация занимает всего несколько строк.

Система событий для игрового движка



Игровые движки - идеальная среда для применения событийно-ориентированного программирования. Представьте, сколько событий происходит в игре: столкновения объектов, нажатия кнопок, окончание анимаций, изменение здоровья персонажей... Вот упрощенный пример системы событий для игры:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// Базовый класс для всех игровых событий
public class GameEvent : EventArgs
{
    public DateTime Timestamp { get; } = DateTime.Now;
}
 
// События, связанные с персонажами
public class CharacterEvent : GameEvent
{
    public Character Character { get; }
 
    public CharacterEvent(Character character)
    {
        Character = character;
    }
}
 
// Конкретное событие - персонаж получил урон
public class CharacterDamagedEvent : CharacterEvent
{
    public int DamageAmount { get; }
    public DamageType DamageType { get; }
 
    public CharacterDamagedEvent(Character character, int damage, DamageType type) : base(character)
    {
        DamageAmount = damage;
        DamageType = type;
    }
}
 
// Менеджер событий - центральная точка управления событиями
public class EventManager
{
    private static EventManager _instance;
    public static EventManager Instance => _instance ??= new EventManager();
 
    // Словарь для хранения всех событий и их подписчиков
    private Dictionary<Type, Delegate> _events = new Dictionary<Type, Delegate>();
 
    // Подписка на определенный тип события
    public void Subscribe<T>(Action<T> handler) where T : GameEvent
    {
        var eventType = typeof(T);
        
        if (_events.TryGetValue(eventType, out var existingHandlers))
        {
            _events[eventType] = Delegate.Combine(existingHandlers, handler);
        }
        else
        {
            _events[eventType] = handler;
        }
    }
 
    // Отписка от события
    public void Unsubscribe<T>(Action<T> handler) where T : GameEvent
    {
        var eventType = typeof(T);
        
        if (_events.TryGetValue(eventType, out var existingHandlers))
        {
            _events[eventType] = Delegate.Remove(existingHandlers, handler);
            
            if (_events[eventType] == null)
            {
                _events.Remove(eventType);
            }
        }
    }
 
    // Публикация события
    public void Publish<T>(T gameEvent) where T : GameEvent
    {
        var eventType = typeof(T);
        
        if (_events.TryGetValue(eventType, out var handlers) && handlers != null)
        {
            // Вызываем всех обработчиков этого типа события
            ((Action<T>)handlers)(gameEvent);
        }
        
        // Также можем вызвать обработчики для базовых типов события
        if (eventType != typeof(GameEvent))
        {
            var baseType = eventType.BaseType;
            while (baseType != null && baseType != typeof(object))
            {
                if (_events.TryGetValue(baseType, out var baseHandlers) && baseHandlers != null)
                {
                    // Создаём метод для приведения типов и вызова базовых обработчиков
                    var method = baseHandlers.GetType().GetMethod("Invoke");
                    method.Invoke(baseHandlers, new object[] { gameEvent });
                }
                
                baseType = baseType.BaseType;
            }
        }
    }
}
Эта система позволяет подписываться на события определённого типа, а также на базовые типы событий. Например, можно подписаться как на все события персонажей (CharacterEvent), так и на конкретные, например, получение урона (CharacterDamagedEvent). Использовать этот менеджер событий можно так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Класс, который реагирует на урон персонажа
public class DamageVisualizer
{
    public DamageVisualizer()
    {
        // Подписываемся на события урона
        EventManager.Instance.Subscribe<CharacterDamagedEvent>(OnCharacterDamaged);
    }
 
    private void OnCharacterDamaged(CharacterDamagedEvent e)
    {
        // Отображаем полученный урон над персонажем
        Console.WriteLine($"{e.Character.Name} получил {e.DamageAmount} урона типа {e.DamageType}");
        
        // Воспроизводим соответствующую анимацию
        if (e.DamageType == DamageType.Fire)
        {
            PlayFireDamageAnimation(e.Character);
        }
    }
 
    private void PlayFireDamageAnimation(Character character)
    {
        // Здесь был бы код воспроизведеня анимации огня
    }
 
    public void Dispose()
    {
        // При уничтожении объекта не забываем отписаться!
        EventManager.Instance.Unsubscribe<CharacterDamagedEvent>(OnCharacterDamaged);
    }
}
Подобная система событий делает код игры модульным и расширяемым. Новые фичи можно добавлять, просто создавая новые подписчики на существующие события, без изменения кода самих издателей. Я использовал похожий подход в одном из своих проектов, и это значительно упростило интеграцию плагинов сторонних разработчиков. Заметьте, что наша система использует обобщённые типы и рефлексию для вызова базовых обработчиков. Это позволяет создавать более гибкую структуру событий с иерархями, но имеет некоторую производительную стоимость. В высоконагруженных сценариях можно оптимизировать этот код, исплльзуя предкомпиляцию делегатов или Expression Trees.

Асинхронная обработка событий с async/await



Современные приложения редко обходятся без асинхронных операций. Скачивание файлов, запросы к базам данных, обработка изображений — все эти задачи потребляют время и ресурсы. Комбинирование событийной модели с асинхронным программированием позволяет создавать отзывчивые приложения, которые не блокируют интерфейс пользователя.
Один из самых интересных трюков, который я освоил в своей практике — создание асинхронных обработчиков событий:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class FileDownloader
{
    public event EventHandler<FileDownloadEventArgs> DownloadCompleted;
    
    public async Task DownloadFileAsync(string url)
    {
        // Имитация скачивания файла
        await Task.Delay(2000);
        var data = new byte[1024]; // Представим, что это скачанные данные
        
        // Вызываем событие из асинхронного метода
        OnDownloadCompleted(new FileDownloadEventArgs(url, data));
    }
    
    protected virtual void OnDownloadCompleted(FileDownloadEventArgs e)
    {
        DownloadCompleted?.Invoke(this, e);
    }
}
 
// Подписка с использованием асинхронного обработчика
fileDownloader.DownloadCompleted += async (sender, e) =>
{
    // Асинхронно обрабатываем скачанный файл
    await ProcessDownloadedFileAsync(e.Data);
    
    // И обновляем UI
    await UpdateUIAsync();
};
Заметьте особенность: сам обработчик события асинхронный. Это чрезвычайно мощная техника, но она имеет одну тонкость — если внутри асинхронного обработчика произойдёт исключение, оно не будет автоматически перехватваться общим механизмом обработки исключений. Поэтому всегда оборачивайте такие обработчики в try-catch:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fileDownloader.DownloadCompleted += async (sender, e) =>
{
    try
    {
        await ProcessDownloadedFileAsync(e.Data);
        await UpdateUIAsync();
    }
    catch (Exception ex)
    {
        // Обрабатываем ошибку
        LogError(ex);
        MessageBox.Show($"Не удалось обработать загруженный файл: {ex.Message}");
    }
};
Без этого, исключения в асинхронных обработчиках могут приводить к падению всего приложения. Тут есть забавная история с продакшн-багом, который мы однажды искали три дня — приложение периодически крашилось, но никаких логов ошибок не было. Причиной оказался именно неперехваченное исключение в асинхронном обработчике события.

Реактивное программирование с Rx.NET



Если вы много работаете с событиями, то рано или поздно столкнётесь с ситуациями, когда стандартный подход начинает "скрипеть". Типичные примеры:
  • Фильтрация событий по условию.
  • Преобразование данных события.
  • Агрегация нескольких событий.
  • Обработка последовательности событий.

Здесь на сцену выходит реактивное программирование и библиотека Rx.NET (Reactive Extensions). Она предоставляет мощный набор операторов для работы с "потоками" событий, превращая их в наблюдаемые последовательности (IObservable<T>).

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Создаём наблюдаемую последовательность из события
var mouseMove = Observable.FromEventPattern<MouseEventArgs>(
    h => form.MouseMove += h,
    h => form.MouseMove -= h);
 
// Трансформируем события в координаты
var mouseCoordinates = mouseMove
    .Select(pattern => pattern.EventArgs.Location);
 
// Фильтруем, берем только каждое 10-е событие для оптимизации
var throttledMouse = mouseCoordinates
    .Where((point, index) => index % 10 == 0);
 
// Подписываемся на отфильтрованную последовательность
var subscription = throttledMouse.Subscribe(point =>
    Console.WriteLine($"Мышь переместилась в {point.X}, {point.Y}"));
 
// Позже отписываемся
subscription.Dispose();
Красота этого подхода в том, что мы работаем с событиями как с коллекциями, применяя знакомые по LINQ операции. Это делает код более декларативным — мы описываем «что» нужно сделать, а не «как» это сделать.
Вот еще пример, который мне особенно нравится — обработка двойных кликов:

C#
1
2
3
4
5
6
7
8
9
10
var clicks = Observable.FromEventPattern<EventArgs>(
    h => button.Click += h,
    h => button.Click -= h);
 
// Определяем двойной клик как два клика в течение 300 мс
var doubleClicks = clicks
    .Buffer(TimeSpan.FromMilliseconds(300))
    .Where(events => events.Count >= 2);
 
doubleClicks.Subscribe(_ => Console.WriteLine("Двойной клик обнаружен!"));
Заметьте, как элегантно решается задача. Нам не нужно вручную отслеживать время между кликами, хранить состояние — все это делает за нас реактивный фреймворк.

Собственные генераторы событий с IObservable



Интерфейс IObservable<T> — это отличный способ инкапсулировать логику генерации событий. Он определяет контракт для объектов, которые могут уведомлять о новых данных, ошибках или завершении последовательности.
Вот пример датчика температуры, который генерирует периодические измерения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public class TemperatureSensor : IObservable<double>
{
    private readonly List<IObserver<double>> _observers = new List<IObserver<double>>();
    private readonly Random _random = new Random();
    private CancellationTokenSource _cts;
    private double _baseTemperature = 20.0; // Базовая температура в Цельсиях
    
    public IDisposable Subscribe(IObserver<double> observer)
    {
        if (!_observers.Contains(observer))
            _observers.Add(observer);
            
        if (_cts == null)
            StartReading();
            
        return new Unsubscriber<double>(_observers, observer);
    }
    
    private async void StartReading()
    {
        _cts = new CancellationTokenSource();
        
        try
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                // Имитируем измерение температуры с небольшими колебаниями
                double currentTemp = _baseTemperature + (_random.NextDouble() * 2 - 1);
                
                foreach (var observer in _observers.ToArray())
                {
                    observer.OnNext(currentTemp);
                }
                
                // Ждем некоторое время перед следующим измерением
                await Task.Delay(1000, _cts.Token);
                
                // Медленно меняем базовую температуру
                _baseTemperature += _random.NextDouble() * 0.1 - 0.05;
            }
        }
        catch (OperationCanceledException)
        {
            // Нормальное завершение при отмене
        }
        catch (Exception ex)
        {
            // Уведомляем подписчиков об ошибке
            foreach (var observer in _observers.ToArray())
            {
                observer.OnError(ex);
            }
        }
        finally
        {
            _cts = null;
        }
    }
    
    public void StopReading()
    {
        _cts?.Cancel();
    }
    
    // Вспомогательный класс для отписки
    private class Unsubscriber<T> : IDisposable
    {
        private readonly List<IObserver<T>> _observers;
        private readonly IObserver<T> _observer;
        
        public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
        {
            _observers = observers;
            _observer = observer;
        }
        
        public void Dispose()
        {
            if (_observer != null && _observers.Contains(_observer))
                _observers.Remove(_observer);
        }
    }
}
А вот как можно использовать этот датчик:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var sensor = new TemperatureSensor();
 
// Создаём наблюдателя
var temperatureDisplay = Observer.Create<double>(
    temp => Console.WriteLine($"Текущая температура: {temp:F1}°C"),
    ex => Console.WriteLine($"Ошибка датчика: {ex.Message}"),
    () => Console.WriteLine("Измерения завершены")
);
 
// Подписываемся на датчик
var subscription = sensor.Subscribe(temperatureDisplay);
 
// ...Позже отписываемся
subscription.Dispose();
Примечательно, что мы можем применять операторы Rx.NET к нашему датчику, например:

C#
1
2
3
4
5
6
7
8
9
10
11
12
// Отфильтровываем только значения выше 21 градуса
var highTemperatures = sensor.Where(temp => temp > 21);
 
// Группируем по целым значениям и вычисляем среднее
var averagesByInteger = sensor
    .GroupBy(temp => (int)temp)
    .Select(group => new { Temperature = group.Key, Average = group.Average() });
 
// Следим за скоростью изменения
var temperatureChanges = sensor
    .Buffer(2, 1) // Буферизуем по 2 значения с шагом 1
    .Select(buffer => buffer.Count < 2 ? 0 : buffer[1] - buffer[0]);
Таким образом, мы не просто получаем значения от датчика, но и выстраиваем сложную цепочку обработки данных. Каждый шаг в этой цепочке описывается декларативно, что делает код более понятным и поддерживаемым. В реальных проектах я использовал подобный подход для мониторинга производительности, обработки данных в реальном времени и создания систем оповещений. IObservable<T> и Rx.NET прекрасно впысываются в концепцию потоков событий и данных, делая код чище и логичнее.

Оптимизация и производительность



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

Производительность делегатов



Начнём с базового вопроса: насколько "дороги" делегаты с точки зрения производительности? Создание делегата — не бесплатная операция. За кулисами происходит выделение памяти для объекта делегата, и если вы создаёте множество экземпляров в цикле или горячем пути вашего кода, это может стать узким местом.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// Неоптимально: создание делегата в цикле
for (int i = 0; i < 10000; i++)
{
    Func<int, int> calculator = x => x * x;
    results[i] = calculator(i);
}
 
// Оптимально: создание делегата один раз
Func<int, int> calculatorOptimized = x => x * x;
for (int i = 0; i < 10000; i++)
{
    results[i] = calculatorOptimized(i);
}
Еще один малоизвестный факт — многоадресные делегаты медленнее одноадресных. Когда у вас есть делегат с множеством подписчиков, каждый вызов .Invoke() проходит через все методы списка подписчиков. Например, в высоконагруженном коде я однажды заменил:

C#
1
2
3
4
5
6
7
8
9
10
// Медленнее, если вызывается часто и имеет много подписчиков
public event EventHandler DataUpdated;
 
// На более прямые вызовы через интерфейс
public interface IDataUpdateListener
{
    void OnDataUpdated(object sender, EventArgs e);
}
 
private readonly List<IDataUpdateListener> _listeners = new List<IDataUpdateListener>();
Это дало ощутимый прирост производительности, хотя и потребовало перестройки части архитектуры.

Сборка мусора и влияние на память



Одна из наиболее коварных проблем, связанных с делегатами, касается взаимодействия со сборщиком мусора. Лямбда-выражения и анонимные методы, захватывающие переменные из внешнего контекста, могут задерживать сборку даже когда, казалось бы, объекты уже не используются.

C#
1
2
3
4
5
6
7
8
9
10
public class ResourceHog
{
    private readonly byte[] _largeArray = new byte[1024 * 1024 * 100]; // 100 MB
    
    public void RegisterCallback(EventPublisher publisher)
    {
        // Эта лямбда захватывает 'this', удерживая весь объект в памяти!
        publisher.SomeEvent += (sender, e) => Console.WriteLine($"Size: {_largeArray.Length}");
    }
}
В этом примере, даже если на ResourceHog больше нет ссылок, он не будет собран сборщиком мусора, пока жив издатель события и не отчищена подписка. А 100 МБ памяти будут заблокированы!

Решения для этой проблемы:
1. Явно отписываться от событий.
2. Использовать слабые события (как мы обсуждали ранее).
3. Не захватывать больше переменных, чем действительно необходимо.

C#
1
2
3
// Лучше: избегаем захвата всего объекта
var arraySize = _largeArray.Length; // Захватываем только то, что нужно
publisher.SomeEvent += (sender, e) => Console.WriteLine($"Size: {arraySize}");

Кэширование делегатов



Интересная техника оптимизации — кэширование часто используемых делегатов, особенно тех, что передаются в методы LINQ или подобные API:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// Без кэширования — для каждого вызова создаётся новый делегат
dataItems.Where(item => item.IsValid)
         .Select(item => item.Value)
         .ToList();
 
// С кэшированием — один делегат для многократного использования
private static readonly Func<DataItem, bool> IsValidFunc = item => item.IsValid;
private static readonly Func<DataItem, double> GetValueFunc = item => item.Value;
 
// Использование
dataItems.Where(IsValidFunc)
         .Select(GetValueFunc)
         .ToList();
Эта техника особенно эффективна в сценариях, где один и тот же фильтр или преобразование применяется многократно.

Паттерн "Event Sourcing"



Event Sourcing — это архитектурный паттерн, где состояние системы определяется последовательностью событий, а не текущим снимком данных. Каждое изменение состояния записывается как событие, которое затем может быть использовано для восстановления состояния в любой момент времени.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class BankAccount
{
    private decimal _balance;
    private readonly List<AccountEvent> _events = new List<AccountEvent>();
    
    public void Deposit(decimal amount)
    {
        var @event = new DepositEvent { Amount = amount, Timestamp = DateTime.Now };
        ApplyEvent(@event);
        _events.Add(@event);
    }
    
    public void Withdraw(decimal amount)
    {
        if (_balance < amount)
            throw new InvalidOperationException("Insufficient funds");
            
        var @event = new WithdrawalEvent { Amount = amount, Timestamp = DateTime.Now };
        ApplyEvent(@event);
        _events.Add(@event);
    }
    
    private void ApplyEvent(AccountEvent @event)
    {
        switch (@event)
        {
            case DepositEvent depositEvent:
                _balance += depositEvent.Amount;
                break;
            case WithdrawalEvent withdrawalEvent:
                _balance -= withdrawalEvent.Amount;
                break;
        }
    }
    
    public void ReplayEvents()
    {
        _balance = 0;
        foreach (var @event in _events)
        {
            ApplyEvent(@event);
        }
    }
}
 
// События
public abstract class AccountEvent
{
    public DateTime Timestamp { get; set; }
}
 
public class DepositEvent : AccountEvent
{
    public decimal Amount { get; set; }
}
 
public class WithdrawalEvent : AccountEvent
{
    public decimal Amount { get; set; }
}
Этот подход имеет множество преимуществ:
1. Полная история изменений.
2. Возможность "перемотать" состояние на любой момент.
3. Упрощенное тестирование и отладка.
4. Естественная поддержка аудита.

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

EventAggregator и управление потоком событий



В крупных системах с множеством событий важно удерживать контроль над их потоком. EventAggregator — ещё один полезный паттерн, который централизует управление событиями:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class EventAggregator
{
    private readonly Dictionary<Type, List<object>> _subscribers = new Dictionary<Type, List<object>>();
    
    public void Subscribe<T>(Action<T> subscriber)
    {
        var type = typeof(T);
        if (!_subscribers.ContainsKey(type))
            _subscribers[type] = new List<object>();
            
        _subscribers[type].Add(subscriber);
    }
    
    public void Publish<T>(T message)
    {
        var type = typeof(T);
        if (!_subscribers.ContainsKey(type))
            return;
            
        foreach (var subscriber in _subscribers[type].OfType<Action<T>>().ToList())
        {
            subscriber(message);
        }
    }
    
    public void Unsubscribe<T>(Action<T> subscriber)
    {
        var type = typeof(T);
        if (!_subscribers.ContainsKey(type))
            return;
            
        _subscribers[type].Remove(subscriber);
    }
}
Преимущества использования EventAggregator:
1. Уменьшение связанности компонентов.
2. Централизованное управление подписками.
3. Возможность легко добавлять кросс-катинг функционал, такой как логирование или отложенное выполнение.

Я применял этот паттерн в крупном проекте, где различные модули должны были взаимодействовать, но не хотелось создавать жесткие зависимости между ними. EventAggregator стал отличным "клеем", соединяющим разные части системы без тесной связи.

Оптимизация событий в многопоточной среде



Многопоточные приложения добавляют дополнительный уровень сложности при работе с событиями. Один из самых больших сюрпризов, который я получил на заре своей карьеры — разнообразные гонки данных (race conditions) из-за неупорядоченных вызовов событий. Представьте ситуацию: несколько потоков почти одновременно вызывают одно и то же событие. Порядок выполнения обработчиков становится недетерминированным, а ведь какой-то обработчик меняет состояние, от которого зависят другие. Внезапно ваша прекрасная система превращается в лотерею из странных багов, которые невозможно воспроизвести.

C#
1
2
3
4
5
6
7
8
9
// Потенциально опасный код в многопоточной среде
public event EventHandler StateChanged;
 
public void UpdateState()
{
    // Изменение состояния и оповещение
    _state = CalculateNewState();
    StateChanged?.Invoke(this, EventArgs.Empty);
}
Для таких случаев, есть несколько приёмов синхронизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Вариант 1: Блокировка на время вызова события
private readonly object _eventLock = new object();
 
public void UpdateState()
{
    // Изменение состояния безопасным образом
    _state = CalculateNewState();
    
    // Блокируем на время вызова события
    lock (_eventLock)
    {
        StateChanged?.Invoke(this, EventArgs.Empty);
    }
}
Этот подход работает, но может вызвать проблемы если обработчики события выполняют длительные операции. В таком случае все остальные потоки будут заблокированы. Альтернативный вариант — использование очередей событий:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Вариант 2: Очередь событий
private readonly ConcurrentQueue<EventData> _eventQueue = new ConcurrentQueue<EventData>();
private readonly AutoResetEvent _processingSignal = new AutoResetEvent(false);
private readonly Thread _processingThread;
 
private class EventData
{
    public object Sender { get; set; }
    public EventArgs Args { get; set; }
    public EventHandler Handler { get; set; }
}
 
public MyClass()
{
    // Создаём и запускаем поток обработки событий
    _processingThread = new Thread(ProcessEvents)
    {
        IsBackground = true,
        Name = "EventProcessingThread"
    };
    _processingThread.Start();
}
 
public void UpdateState()
{
    _state = CalculateNewState();
    
    // Вместо прямого вызова, добавляем в очередь
    var handler = StateChanged;
    if (handler != null)
    {
        _eventQueue.Enqueue(new EventData
        {
            Sender = this,
            Args = EventArgs.Empty,
            Handler = handler
        });
        _processingSignal.Set(); // Сигнализируем о новом событии
    }
}
 
private void ProcessEvents()
{
    while (true)
    {
        // Ждём сигнала о новых событиях
        _processingSignal.WaitOne();
        
        // Обрабатываем все события в очереди
        while (_eventQueue.TryDequeue(out var eventData))
        {
            try
            {
                eventData.Handler(eventData.Sender, eventData.Args);
            }
            catch (Exception ex)
            {
                // Логируем ошибку, но продолжаем обработку
                Console.WriteLine($"Error in event handler: {ex.Message}");
            }
        }
    }
}
Этот подход обеспечивает последовательную обработку событий, но имет цену — определённую задержку между возникновением события и его обработкой.

Профилирование и отладка систем, основаных на событиях



Отладка событийно-ориентированных систем может быть настоящей головной болью. Когда код выполняется по событиям из разных источников, отслеживать поток выполнения становится непросто. Иногда единственное, что спасает при отладке такого кода — временные обертки для просмотра всех вызовов событий:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Временная обёртка для отладки вызовов события
public event EventHandler<OrderEventArgs> OrderProcessed
{
    add
    {
        Console.WriteLine($"[DEBUG] Подписка на OrderProcessed: {value.Method.Name}");
        _orderProcessed += value;
    }
    remove
    {
        Console.WriteLine($"[DEBUG] Отписка от OrderProcessed: {value.Method.Name}");
        _orderProcessed -= value;
    }
}
 
private EventHandler<OrderEventArgs> _orderProcessed;
 
protected virtual void OnOrderProcessed(OrderEventArgs e)
{
    Console.WriteLine($"[DEBUG] Вызов OrderProcessed, заказ №{e.OrderId}");
    _orderProcessed?.Invoke(this, e);
}
Для серьезного профилирования лучше использовать специализированные инструменты, такие как Visual Studio Profiler, dotTrace или ANTS Performance Profiler. Они позволяют отследить, сколько времени занимают вызовы делегатов и обработчиков событий.

В одном из проектов мы обнаружили, что 40% времени выполнения приложения уходило на обработку одного события, которое вызывалось 100 раз в секунду и имело 12 обработчиков. Оптимизация этого узкого места дала колоссальный прирост производительности. Очень полезный приём — временная замена события на метрики для мониторинга:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Stopwatch _sw = new Stopwatch();
private long _totalEventTime = 0;
private int _eventCallCount = 0;
 
protected virtual void OnDataReceived(DataReceivedEventArgs e)
{
    _sw.Restart();
    DataReceived?.Invoke(this, e);
    _sw.Stop();
    
    // Собираем статистику
    Interlocked.Add(ref _totalEventTime, _sw.ElapsedMilliseconds);
    Interlocked.Increment(ref _eventCallCount);
    
    // Периодически выводим отчёт
    if (_eventCallCount % 1000 == 0)
    {
        Console.WriteLine($"Event stats: {_eventCallCount} calls, avg {_totalEventTime / _eventCallCount} ms per call");
    }
}
При работе с делегатами и событиями, всегда помните золотое правило оптимизации: измерять, оптимизировать, измерять снова. Только так вы будете уверены, что ваши изменения действительно улучшают ситуацию, а не создают новые проблемы.

Горизонты событийного программирования в C#



Когда-то давно я считал, что делегаты — это просто причуда языка, усложняющая понимание кода. Сейчас же я понимаю, что без них пришлось бы писать тонны шаблонного кода и создавать запутаные деревья наследования, чтобы добиться той же гибкости. Могу с уверенностью сказать — овладение этим инструментарием принесло революцию в то, как я проектирую и реализую системы. Самое замечательное в событиях и делегатах то, что они продолжают эволюционировать вместе с языком. От "голых" делегатов до лямбда-выражений, от стандартных событий до реактивных потоков — мы наблюдаем постоянное совершенствование этих концепций.

В конце концов, событийно-ориентрованое програмирование это не просто набор технологий, а целая философия разработки, меняющая наш подход к структурированию кода. Она учит нас мыслить в терминах сообщений и реакций, состояний и переходов, что идеально подходит для современных распределённых, многопоточных и интерактивных приложений.

Анимация State Driven Camera
Всем привет. Подскажите пожалуйста, можно ли под State Driven Camera создать что-то вроде анимации...

WebBrowser не поддерживает Event MouseDown и Event MouseUp
Здравствуйте, у меня имеется WebBrowser control в windowsFormApp, но он не поддерживает Event...

события, делегаты.
как можно отловаить событие изменения значения? допустим есть класс class first{ int var;...

делегаты и события. Как их использовать и где?
вот занимаюсь по книжке С#3.0 справочник братьев Албахари и что-то засел с делегатами и событиями....

Делегаты и события
Нужно создать программу в которой будет Форма (WinForm) и исполняющий код в Program.cs Суть задачи...

Делегаты и события
Помогите дописать программу!!! Задание: Имеется интерфейс, в котором два пользователя...

Объяснить простым языком, что есть делегаты и события
Разъясните пожалуйста тему о делегатах и события подробнее по книге плохо понимаю

Делегаты и события
Разъясните пожалуйста мне тему делегатов и событий , по учебнику плохо понимаю

Привязать делегаты события одного класса к другому
Как привязать делегаты события одного класса к другому? Есть класс А и класс Б. В обоих классах...

классы, делегаты и события
Добрый день, ребята Необходимо написать мелкую программу учета покупок. Допустим есть классы...

делегаты и события
Здравствуйте! Помогите пожалуйста с задачей: Есть два делегата: delegate void SimpleDel(string...

Как работают делегаты и события?
Уже несколько дней пытаюсь изучить как работают делегаты и события, но так до конца и не могу...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Чем асинхронная логика (схемотехника) лучше тактируемой, как я думаю, что помимо энергоэффективности - ещё и безопасность.
Hrethgir 14.05.2025
Помимо огромного плюса в энергоэффективности, асинхронная логика - тотальный контроль над каждым совершённым тактом, а значит - безусловная безопасность, где безконтрольно не совершится ни одного. . .
Многопоточные приложения на C++
bytestream 14.05.2025
C++ всегда был языком, тесно работающим с железом, и потому особеннно эффективным для многопоточного программирования. Стандарт C++11 произвёл революцию, добавив в язык нативную поддержку потоков,. . .
Stack, Queue и Hashtable в C#
UnmanagedCoder 14.05.2025
Каждый опытный разработчик наверняка сталкивался с ситуацией, когда невинный на первый взгляд List<T> превращался в узкое горлышко всего приложения. Причина проста: универсальность – это прекрасно,. . .
Как использовать OAuth2 со Spring Security в Java
Javaican 14.05.2025
Протокол OAuth2 часто путают с механизмами аутентификации, хотя по сути это протокол авторизации. Представьте, что вместо передачи ключей от всего дома вашему другу, который пришёл полить цветы, вы. . .
Анализ текста на Python с NLTK и Spacy
AI_Generated 14.05.2025
NLTK, старожил в мире обработки естественного языка на Python, содержит богатейшую коллекцию алгоритмов и готовых моделей. Эта библиотека отлично подходит для образовательных целей и. . .
Реализация DI в PHP
Jason-Webb 13.05.2025
Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о. . .
Обработка изображений в реальном времени на C# с OpenCV
stackOverflow 13.05.2025
Объединение библиотеки компьютерного зрения OpenCV с современным языком программирования C# создаёт симбиоз, который открывает доступ к впечатляющему набору возможностей. Ключевое преимущество этого. . .
POCO, ACE, Loki и другие продвинутые C++ библиотеки
NullReferenced 13.05.2025
В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .
Паттерны проектирования GoF на C#
UnmanagedCoder 13.05.2025
Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of. . .
Создаем CLI приложение на Python с Prompt Toolkit
py-thonny 13.05.2025
Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru