Книга: C# 4.0: полное руководство

События

События

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

События являются членами класса и объявляются с помощью ключевого слова event. Чаще всего для этой цели используется следующая форма:

event делегат_события имя_события;

где делегат_события обозначает имя делегата, используемого для поддержки события, а имя_событмя — конкретный объект объявляемого события.

Рассмотрим для начала очень простой пример.

// Очень простой пример, демонстрирующий событие,
using System;
// Объявить тип делегата для события,
delegate void MyEventHandler();
// Объявить класс, содержащий событие,
class MyEvent {
  public event MyEventHandler SomeEvent;
  // Этот метод вызывается для запуска события,
  public void OnSomeEvent() {
    if (SomeEvent != null)
    SomeEvent();
  }
}
class EventDemo {
// Обработчик события,
  static void Handler() {
    Console.WriteLine("Произошло событие");
  }
  static void Main() {
    MyEvent evt = new MyEvent();
    // Добавить метод Handler() в список событий,
    evt.SomeEvent += Handler;
    // Запустить событие,
    evt.OnSomeEvent();
  }
}

Вот какой результат получается при выполнении этого кода.

Произошло событие

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

delegate void MyEventHandler();

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

Далее создается класс события MyEvent. В этом классе объявляется событие SomeEvent в следующей строке кода.

public event MyEventHandler SomeEvent;

Обратите внимание на синтаксис этого объявления. Ключевое слово event уведомляет компилятор о том, что объявляется событие.

Кроме того, в классе MyEvent объявляется метод OnSomeEvent(), вызываемый для сигнализации о запуске события. Это означает, что он вызывается, когда происходит событие. В методе OnSomeEvent() вызывается обработчик событий с помощью делегата SomeEvent.

if(SomeEvent != null)
  SomeEvent();

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

В классе EventDemo создается обработчик событий Handler(). В данном простом примере обработчик событий просто выводит сообщение, но другие обработчики могут выполнять более содержательные функции. Далее в методе Main() создается объект класса события MyEvent, a Handler() регистрируется как обработчик этого события, добавляемый в список.

MyEvent evt = new MyEvent();
// Добавить метод Handler() в список событий,
evt.SomeEvent += Handler;

Обратите внимание на то, что обработчик добавляется в список с помощью оператора +=. События поддерживают только операторы += и -=. В данном случае метод Handler() является статическим, но в качестве обработчиков событий могут также служить методы экземпляра.

И наконец, событие запускается, как показано ниже.

// Запустить событие,
evt.OnSomeEvent();

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

Пример групповой адресации события

Как и делегаты, события поддерживают групповую адресацию. Это дает возможность нескольким объектам реагировать на уведомление о событии. Ниже приведен пример групповой адресации события.

// Продемонстрировать групповую адресацию события,
using System;
// Объявить тип делегата для события,
delegate void MyEventHandler() ;
// Объявить делегат, содержащий событие,
class MyEvent {
  public event MyEventHandler SomeEvent;
  // Этот метод вызывается для запуска события,
  public void OnSomeEvent() {
    if(SomeEvent != null)
      SomeEvent();
  }
}
class X {
  public void Xhandler() {
    Console.WriteLine("Событие получено объектом класса X");
  }
}
class Y {
  public void Yhandler() {
    Console.WriteLine("Событие получено объектом класса Y");
  }
}
class EventDemo2 {
  static void Handler() {
    Console.WriteLine("Событие получено объектом класса EventDemo");
  }
  static void Main() {
    MyEvent evt = new MyEvent();
    X xOb = new X();
    Y yOb = new Y();
    // Добавить обработчики в список событий.
    evt.SomeEvent += Handler;
    evt.SomeEvent += xOb.Xhandler;
    evt.SomeEvent += yOb.Yhandler;
    // Запустить событие, evt.OnSomeEvent() ;
    Console.WriteLine() ;
    // Удалить обработчик.
    evt.SomeEvent -= xOb.Xhandler;
    evt.OnSomeEvent() ;
  }
}

При выполнении кода этого примера получается следующий результат.

Событие получено объектом класса EventDemo
Событие получено объектом класса X
Событие получено объектом класса Y
Событие получено объектом класса EventDemo
Событие получено объектом класса Y

В данном примере создаются два дополнительных класса, X и Y, в которых также определяются обработчики событий, совместимые с делегатом MyEventHandler. Поэтому эти обработчики могут быть также включены в цепочку событий. Обратите внимание на то, что обработчики в классах X и Y не являются статическими. Это означает, что сначала должны быть созданы объекты каждого из этих классов, а затем в цепочку событий должны быть введены обработчики, связанные с их экземплярами. Об отличиях между обработчиками экземпляра и статическими обработчиками речь пойдет в следующем разделе.

Методы экземпляра в сравнении со статическими методами в качестве обработчиков событий

Методы экземпляра и статические методы могут быть использованы в качестве обработчиков событий, но между ними имеется одно существенное отличие. Когда статический метод используется в качестве обработчика, уведомление о событии распространяется на весь класс. А когда в качестве обработчика используется метод экземпляра, то события адресуются конкретным экземплярам объектов. Следовательно, каждый объект определенного класса, которому требуется получить уведомление о событии, должен быть зарегистрирован отдельно. На практике большинство обработчиков событий представляет собой методы экземпляра, хотя это, конечно, зависит от конкретного приложения. Рассмотрим применение каждой из этих двух разновидностей методов в качестве обработчиков событий на конкретных примерах.

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

/* Уведомления о событиях получают отдельные объекты, когда метод экземпляра используется в качестве обработчика событий. */
using System;
// Объявить тип делегата для события,
delegate void MyEventHandler() ;
// Объявить класс, содержащий событие,
class MyEvent {
  public event MyEventHandler SomeEvent;
  // Этот метод вызывается для запуска события,
  public void OnSomeEvent() {
    if(SomeEvent != null)
      SomeEvent() ;
  }
}
class X {
  int id;
  public X(int x) {
    id = x;
  }
  // Этот метод экземпляра предназначен в качестве обработчика событий,
  public void Xhandler() {
    Console.WriteLine("Событие получено объектом " + id);
  }
}
class EventDemo3 {
  static void Main() {
    MyEvent evt = new MyEvent();
    X ol = new X(1);
    X o2 = new X(2);
    X o3 = new X(3);
    evt.SomeEvent += ol.Xhandler;
    evt.SomeEvent += o2.Xhandler;
    evt.SomeEvent += o3.Xhandler;
    // Запустить событие,
    evt.OnSomeEvent() ;
  }
}

Выполнение кода из этого примера приводит к следующему результату.

Событие получено объектом 1
Событие получено объектом 2
Событие получено объектом 3

Как следует из результата выполнения кода из приведенного выше примера, каждый объект должен зарегистрировать свой интерес в событии отдельно, и тогда он будет получать отдельное уведомление о событии.

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

/* Уведомления о событии получает класс, когда статический метод используется в качестве обработчика событий. */
using System;
// Объявить тип делегата для события,
delegate void MyEventHandler();
// Объявить класс, содержащий событие,
class MyEvent {
  public event MyEventHandler SomeEvent;
  // Этот метод вызывается для запуска события,
  public void OnSomeEvent() {
    if (SomeEvent != null)
      SomeEvent() ;
  }
}
class X {
/* Этот статический метод предназначен в качестве обработчика событий. */
  public static void Xhandler() {
    Console.WriteLine("Событие получено классом.");
  }
}
class EventDemo4 {
  static void Main() {
    MyEvent evt = new MyEvent();
    evt.SomeEvent += X.Xhandler;
    // Запустить событие,
    evt.OnSomeEvent();
  }
}

При выполнение кода этого примера получается следующий результат.

Событие получено классом.

Обратите в данном примере внимание на то, что объекты класса X вообще не создаются. Но поскольку Xhandler() является статическим методом класса X, то он может быть привязан к событию SomeEvent и выполнен при вызове метода OnSomeEvent().

Применение аксессоров событий

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

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

event делегат_события имя_ события {
  add {
    // Код добавления события в цепочку событий.
  }
  remove {
    // Код удаления события из цепочки событий.
  }
}

В эту форму входят два аксессора событий: add и remove. Аксессор add вызывается, когда обработчик событий добавляется в цепочку событий с помощью оператора +=. В то же время аксессор remove вызывается, когда обработчик событий удаляется из цепочки событий с помощью оператора -=.

Когда вызывается аксессор add или remove, он принимает в качестве параметра добавляемый или удаляемый обработчик. Как и в других разновидностях аксессоров, этот неявный параметр называется value. Реализовав аксессоры add или remove, можно организовать специальную схему хранения обработчиков событий. Например, обработчики событий можно хранить в массиве, стеке или очереди.

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

// Создать специальные средства для управления списками
// вызова обработчиков событий.
using System;
// Объявить тип делегата для события.
delegate void MyEventHandler();
// Объявить класс для хранения максимум трех событий,
class MyEvent {
  MyEventHandler[] evnt = new MyEventHandler[3];
  public event MyEventHandler SomeEvent {
    // Добавить событие в список,
    add {
      int i;
      for(i=0; i < 3; i++)
        if(evnt[i] == null) {
          evnt[i] = value;
          break;
        }
      if (i == 3) Console.WriteLine("Список событий заполнен.");
    }
    // Удалить событие из списка,
    remove {
      int i;
      for(i=0; i < 3; i++)
        if(evnt[i] == value) {
          evnt[i] = null;
          break;
      }
      if (i == 3) Console.WriteLine("Обработчик событий не найден.");
    }
  }
  // Этот метод вызывается для запуска событий,
  public void OnSomeEvent()    {
    for(int i=0; i < 3; i++)
      if(evnt[i] != null) evnt[i]();
  }
}
// Создать ряд классов, использующих делегат MyEventHandler.
class W {
  public void Whandler() {
    Console.WriteLine("Событие получено объектом W");
  }
}
class X {
  public void Xhandler() {
    Console.WriteLine("Событие получено объектом X");
  }
}
class Y {
  public void Yhandler() {
    Console.WriteLine("Событие получено объектом Y");
  }
}
class Z {
  public void Zhandler() {
    Console.WriteLine("Событие получено объектом Z");
  }
}
class EventDemo5 {
  static void Main() {
    MyEvent evt = new MyEvent();
   W wOb = new W(); 
    X xOb = new X(); 
    Y yOb = new Y(); 
    Z zOb = new Z();
    // Добавить обработчики в цепочку событий.
    Console.WriteLine("Добавление событий.");
    evt.SomeEvent += wOb.Whandler;
    evt.SomeEvent += xOb.Xhandler;
    evt.SomeEvent += yOb.Yhandler;
    // Сохранить нельзя - список заполнен,
    evt.SomeEvent += zOb.Zhandler;
    Console.WriteLine();
    // Запустить события,
    evt.OnSomeEvent();
    Console.WriteLine();
    // Удалить обработчик.
    Console.WriteLine("Удаление обработчика xOb.Xhandler.");
    evt.SomeEvent -= xOb.Xhandler;
    evt.OnSomeEvent();
    Console.WriteLine();
    // Попробовать удалить обработчик еще раз.
    Console.WriteLine("Попытка удалить обработчик " +
           "xOb.Xhandler еще раз.");
    evt.SomeEvent -= xOb.Xhandler;
    evt.OnSomeEvent();
    Console.WriteLine();
    //А теперь добавить обработчик Zhandler.
    Console.WriteLine("Добавление обработчика zOb.Zhandler.");
    evt.SomeEvent += zOb.Zhandler;
    evt.OnSomeEvent();
  }
}

Ниже приведен результат выполнения этой программы

Добавление событий.
Список событий заполнен.
Событие получено объектом W
Событие получено объектом X
Событие получено объектом Y
Удаление обработчика xOb.Xhandler.
Событие получено объектом W
Событие получено объектом Y
Попытка удалить обработчик xOb.Xhandler еще раз.
Обработчик событий не найден.
Событие получено объектом W
Событие получено объектом Y
Добавление обработчика zOb.Zhandler.
Событие получено объектом W
Событие получено объектом X
Событие получено объектом Y

Рассмотрим данную программу более подробно. Сначала в ней определяется делегат обработчиков событий MyEventHandler. Затем объявляется класс MyEvent. В самом его начале определяется массив обработчиков событий evnt, состоящий из трех элементов.

MyEventHandler[] evnt = new MyEventHandler[3];

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

Далее объявляется событие SomeEvent. В этом объявлении используется приведенная ниже аксессорная форма оператора event.

public event MyEventHandler SomeEvent {
  // Добавить событие в список,
  add {
    int i;
    for(i=0; i < 3; i++)
      if(evnt[i] == null) {
        evnt[i] = value;
        break;
      }
    if (i == 3) Console.WriteLine("Список событий заполнен.");
  }
  // Удалить событие из списка,
  remove {
    int i;
    for(i=0; i < 3; i++)
      if(evnt[i] == value) {
        evnt[i] = null;
        break;
      }
  }
}

Когда в цепочку событий добавляется обработчик событий, вызывается аксессор add, и в первом неиспользуемом (т.е. пустом) элементе массива evnt запоминается ссылка на этот обработчик, содержащаяся в неявно задаваемом параметре value. Если в массиве отсутствуют свободные элементы, то выдается сообщение об ошибке. (Разумеется, в реальном коде при переполнении списка лучше сгенерировать соответствующее исключение.) Массив evnt состоит всего из трех элементов, поэтому в нем можно сохранить только три обработчика событий. Когда же обработчик событий удаляется из цепочки событий, то вызывается аксессор remove и в массиве evnt осуществляется поиск ссылки на этот обработчик, передаваемой в качестве параметра value. Если ссылка найдена, то соответствующему элементу массива присваивается пустое значение (null), а значит, обработчик удаляется из цепочки событий.

При запуске события вызывается метод OnSomeEvent(). В этом методе происходит циклическое обращение к элементам массива evnt для вызова по очереди каждого обработчика событий.

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

------------------------------------

ПРИМЕЧАНИЕ

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

------------------------------------

Разнообразные возможности событий

События могут быть определены и в интерфейсах. При этом события должны предоставляться классами, реализующими интерфейсы. События могут быть также определены как абстрактные (abstract). В этом случае конкретное событие должно быть реализовано в производном классе. Но аксессорные формы событий не могут быть абстрактными. Кроме того, событие может быть определено как герметичное (sealed). И наконец, событие может быть виртуальным, т.е. его можно переопределить в производном классе.

Оглавление книги


Генерация: 0.041. Запросов К БД/Cache: 0 / 2
поделиться
Вверх Вниз