• Microsoft .NET
  • C#.NET
  • Основы, лучшие методы и соглашения реализации событий в C#

Основы, лучшие методы и соглашения реализации событий в C# - Аргументы события, синтаксис объявления и код возбуждения события

ОГЛАВЛЕНИЕ

5. Аргументы события (EventArgs)

Аргументы события – иногда именуемые "event args" – образуют данные, отправляемые публикатором события подписчикам в ходе возбуждения события. Видимо, эти данные относятся к возникновению события. Например, когда возбуждается событие "файл был только что удален", аргументы события, скорее всего, будут состоять из имени файла до изменения имени, а также имени файла после изменения его имени. Методы обработки события могут считывать аргументы события (называемые "данные события"), чтобы узнать больше о возникновении события.

5.1 Роль System.EventArgs

Есть два основных варианта включения аргументов события в ваши события.
1.    Вы можете инкапсулировать все аргументы события в виде свойств класса, полученного от System.EventArgs. Во время выполнения экземпляр этого класса отправляется подписчикам события, когда событие возбуждено. Подписчики события считывают аргументы события в виде свойств этого класса.
2.    Вы можете избежать использования System.EventArgs и взамен объявить отдельные аргументы события - подобно тому, как вы включили бы аргументы в объявление метода. Причины невыгодности такого подхода описаны в разделе 5.2.

Первый указанный выше вариант настоятельно рекомендуется, и его поддержка встроена в среду разработки .NET с помощью класса System.EventArgs. События, реализованные в компонентах среды разработки .NET, по соглашению, предоставляют свои аргументы события в виде экземпляров System.EventArgs, или в виде специфичных для события подклассов System.EventArgs.

Некоторые события не содержат никаких данных. В этих случаях System.EventArgs используется как метка-заполнитель, в основном для сохранения одной единообразной сигнатуры обработчика события во всех событиях, независимо от того, содержат события данные или нет. В случае событий без данных публикатор события отправляет значение System.EventArgs.Empty во время возбуждения события.

5.2 Расширение System.EventArgs

Существование System.EventArgs и его рекомендуемые использования поддерживают соглашения о реализации событий. Безусловно, публикатор события может задавать данные события без использования System.EventArgs или любого его подкласса. В таких случаях сигнатура делегата может задавать тип и имя каждого параметра. Но недостаток данного подхода в том, что такая сигнатура связывает публикатор события со всеми подписчиками. Если вы хотите изменять параметры в дальнейшем, то вам также придется изменять всех подписчиков. Поэтому рекомендуется инкапсулировать все данные события в подклассе класса System.EventArgs, так как такой подход сокращает объем работы, требуемый для последующего изменения количества и типов значений, отправляемых подписчикам события.

Чтобы показать компромиссы, связанные с отправкой экземпляра подкласса System.EventArgs по сравнению с отправкой отдельных аргументов события, рассмотрим сценарий, в котором вам нужно добавить единственное значение типа string в данные события. Если бы вы задали данные события в виде отдельных параметров в сигнатуре делегата (а не путем создания производного класса от System.EventArgs), то все подписчики вашего события пришлось бы изменять, чтобы они принимали один дополнительный параметр string. Даже подписчики, не интересующиеся этим дополнительным значением string, пришлось бы изменить, чтобы они принимали это значение, так как сигнатура пользовательского обработчика событий изменилась бы. Если бы вместо этого вы создали производный класс от System.EventArgs, то вам пришлось бы лишь добавить новое свойство string в ваш класс. Сигнатура события не изменилась бы, и, следовательно, не изменилась бы сигнатура обработчика события, используемая в любом из существующих подписчиков. Подписчики, не интересующиеся новым свойством string, не пришлось бы изменять, так как сигнатура обработчика события не изменилась бы – и они могли бы игнорировать дополнительное свойство string. Подписчики, интересующиеся новым значением string, смогли бы считать его в виде свойства подкласса EventArgs.

Здесь приведен пример подкласса EventArgs, инкапсулирующего единственное значение string:

public class FileDeletedEventArgs : System.EventArgs 
{
   // Поле
   string m_FileName = string.empty;

   // Конструктор
   FileDeletedEventArgs(string fileName)
   {
      m_FileName = fileName;
   }

   // Свойство
   public string FileName
   {
      get { return m_FileName; }
   }
}

5.3 Роль System.ComponentModel.CancelEventArgs

System.ComponentModel.CancelEventArgs получен от System.EventArgs, и существует для поддержки отменяемых событий. Кроме членов, предоставляемых EventArgs, CancelEventArgs предоставляет булево свойство Cancel, которое при установке в true подписчиком события, используется публикатором события для отмены события.

Раздел 16 данной статьи более подробно описывает отменяемые события (нажмите здесь, чтобы перейти туда сейчас).

6. Синтаксис объявления события

6.1 Варианты синтаксиса объявления события

Ключевое слово event используется для формального объявления события. Есть два правильных варианта синтаксиса объявления события. Независимо от того, какой синтаксис вы примените, компилятор C# преобразует оба объявления свойства в следующие три компонента в выходной сборке.

1.    Обработчик события с закрытой областью действия (или функционально эквивалентная структура данных). Делегат имеет закрытую область действия, чтобы запретить вызов события внешним кодом, тем самым сохраняя инкапсуляцию.
2.    Метод Add с открытой областью действия; используется для добавления подписчиков в закрытый обработчик события.
3.    Метод Remove с открытой областью действия, применяемый для удаления подписчиков из закрытого обработчика события.

1. Полеподобный синтаксис

public event TheEventHandler MyEvent;

Полеподобный синтаксис объявляет событие в одной или двух строках кода (одна строка- для события, другая- для связанного с ним обработчика события, если/когда не используется встроенный делегат EventHandler).

2. Свойствоподобный синтаксис

public event TheEventHandler MyEvent
{
   add
   {
      // код здесь добавляет входной экземпляр делегата в нижележащий список
      // обработчиков события
   }
   remove
   {
      // код здесь удаляет экземпляр делегата из нижележащего списка
      // обработчиков события
   }
}

Свойствоподобный синтаксис выглядит очень похожим на типичное объявление свойства, но с явно заданными блоками add и remove вместо блоков "получатель" и "установщик". Вместо того, чтобы возвращать или устанавливать значение закрытой переменной члена, они добавляют и удаляют входные экземпляры делегата в/из нижележащего обработчика события или другой структуры данных, играющей аналогичную роль.

Рекомендации по организации поточной обработки

Полеподобный синтаксис автоматически является поточно-ориентированным:

public event FileDeletedHandler FileDeleted;

Свойствоподобный синтаксис придется специально делать поточно-ориентированным. Ниже приведен поточно-ориентированный вариант:

private readonly object padLock = new object();

public event System.EventHandler<filedeletedeventargs />FileDeleted
{
   add
   {
      lock (padLock)
      {
         FileDeleted += value;
      }
   }
   remove
   {
      lock (padLock)
      {
         FileDeleted -= value;
      }
   }
}

Вы можете опустить блоки lock{} и объявление переменной padlock, если потоковая безопасность не требуется.

6. 2 Рекомендации по выбору между полеподобным синтаксисом и свойствоподобным синтаксисом

Выбирая из вариантов синтаксиса, учитывайте, что свойствоподобный синтаксис дает вам больше контроля над реализацией вашего события, чем доступно при полеподобном синтаксисе. Хотя полеподобный синтаксис будет скомпилирован в IL, очень похожий на IL, сгенерированный для свойствоподобного синтаксиса, полеподобный синтаксис не дает вам таких же возможностей для явного контроля реализации события.

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

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

6.3 Механизм публикации/подписки при помощи делегатов без событий (никогда не делайте так)

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

Хотя вы можете пропустить ключевое слово "событие" (и, соответственно, формальное объявление события) и использовать открытый делегат для предоставления механизма уведомлений о публикации-и-подписке, вы никогда не должны так делать. Недостаток открытых делегатов (по сравнению с объявлением события) в том, что методы вне публикующего класса могут заставлять делегаты с открытой областью действия вызывать методы, на которые ссылаются данные делегаты. Это нарушает базовые принципы инкапсуляции и может быть источником серьезных трудно устранимых проблем (ситуация гонки, и т.д.). Поэтому вы должны реализовывать события только с помощью ключевого слова event. Когда делегаты реализуются для поддержки событий, делегат – даже если он объявлен в качестве открытого члена определяющего класса – может быть вызван только изнутри класса (путем возбуждения события), а другие классы могут только подписываться на и отписываться от нижележащего делегата с помощью события.

7. Код возбуждения события

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

protected virtual void OnMailArrived(MailArrivedEventArgs) 
{
   // Здесь возбуждается событие
}

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

Если подписчики отсутствуют, делегат будет проверен на null. Следующий код возбуждает событие, но только если событие имеет каких-либо подписчиков.

if (MyEvent != null) 
{
   MyEvent(this, EventArgs.Empty);
}

Есть вероятность, что событие может быть сброшено (кодом, выполняющимся в другом потоке) между проверкой на null и строкой, действительно возбуждающей событие. Этот сценарий создает ситуацию гонки. Поэтому рекомендуется создавать, проверять и возбуждать копию обработчика события (делегата) таким образом:

MyEventHandler handler = MyEvent;
 
if (handler != null)
{
   handler (this, EventArgs.Empty)
}

Любые необработанные исключения, возбужденные в методах обработки события в подписчиках, будут переданы публикатору события. Поэтому возбуждение события должно выполняться только внутри блока try/catch:

public void RaiseTheEvent(MyEventArgs eventArgs) 
{
   try
   {
      MyEventHandler handler = MyEvent;
      if (handler != null)
      {
         handler (this, eventArgs)
      }
   }
   catch
   {
      // Здесь обрабатываются исключения
   }
}

События могут иметь несколько подписчиков, каждый из которых по очереди вызывается обработчиком события (делегатом), когда обработчик события вызывается с помощью строки [handler (this, eventArgs)]. Обработчик события, используемый в вышеприведенном блоке кода, перестает повторять свой список вызовов (подписанных на него методов обработки события), когда первое необработанное исключение возбуждается подписчиком. Например, если имеется 3 подписчика, и 2-й подписчик выбрасывает необработанное исключение при вызове его делегатом, то 3-й подписчик вообще не получит уведомления о событии. Если вы хотите, чтобы все подписчики получали уведомление о событии, даже если другие подписчики выбрасывают необработанные исключения, воспользуйтесь следующим кодом, явно проходящим в цикле по списку вызовов обработчика события:

public void RaiseTheEvent(MyEventArgs eventArgs) 
{
   MyEventHandler handler = MyEvent;
   if (handler != null)
   {
      Delegate[] eventHandlers = handler.GetInvocationList();
      foreach (Delegate currentHandler in eventHandlers)
      {
         MyEventHandler currentSubscriber = (MyEventHandler)currentHandler;
         try
         {
            currentSubscriber(this, eventArgs);
         }
         catch (Exception ex)
         {
            // Здесь обрабатывается исключение.
         }
      }
   }
}