Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2376 / 262 | Оценка: 4.47 / 4.24 | Длительность: 14:45:00
Лекция 8:

Классы с событиями

< Лекция 7 || Лекция 8: 12345 || Лекция 9 >
Аннотация: Поля, методы и события – треугольник, полностью описывающий объекты класса. События позволяют специфицировать поведение объектов класса, поскольку каждый объект при возникновении события может иметь свой набор обработчиков этого события. Лекция сопровождается задачами.

Проект к данной лекции Вы можете скачать здесь.

Специфика поведения объектов

Каждый объект является экземпляром некоторого класса. Класс задает свойства и поведение своих экземпляров. Методы класса определяют поведение объектов, свойства - их состояние. Все объекты обладают одними и теми же методами. Можно полагать, что методы задают врожденное поведение объектов. У всех объектов один и тот же набор свойств, но значения свойств объектов различны, так что объекты одного класса находятся в разных состояниях. Объекты класса " Человек " могут иметь разные значения свойства " Рост ": один - высокий, другой - низкий, третий - среднего роста.

Хотя реализация методов у всех объектов одна, это не значит, что результат выполнения одного и того же метода, вызванного разыми объектами, будет один и тот же, поскольку выполнение метода зависит от свойств. Так что методы " Пробежать километр " и " Решить задачу " будут давать разные результаты для разных объектов класса " Человек ".

Как сделать поведение объектов специфическим? Как добавить им поведение, характерное для данного объекта? С программистской точки зрения это означает добавление нового метода, которого нет у других объектов этого класса. Один из наиболее известных путей - это наследование. Можно создать класс-наследник, у которого, наряду с унаследованным родительским поведением, будут и собственные методы. Например, наследником класса " Человек " может быть класс " Человек_образованный ", обладающий методами: читать и писать, считать и программировать. Поведение объектов потомка, имеющих собственные методы, отличается от поведения объектов родительского класса.

Есть еще один механизм, позволяющий объектам вести себя по-разному в одних и тех же обстоятельствах. Это механизм событий, рассмотрением которого сейчас и займемся. Класс, помимо свойств и методов, может иметь события. Содержательно, событием является некоторое специальное состояние, в котором может оказаться объект класса. Так, для объектов класса " Человек " событием может быть рождение или смерть, свадьба или развод. О событиях в мире программных объектов чаще всего говорят в связи с интерфейсными объектами, у которых события возникают по причине действий пользователя. Так, командная кнопка может быть нажата пользователем - в результате у кнопки возникнет событие Click, документ может быть закрыт - событие Close, в список может быть добавлен новый элемент - событие Changed. Набор событий у всех объектов одного класса один и тот же, но вот методы, обрабатывающие возникшие события, могут быть разные. Если наследование позволяет задать характерное поведение для некоторого класса объектов, то события позволяют задать индивидуальное поведение объекта в специфических ситуациях, когда возникает некоторое событие. Две командные кнопки, посаженные на форму, ведут себя по-разному при возникновении события Click, поскольку обработчики события для этих объектов задаются разными методами.

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

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

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

Зажигая событие, объект класса посылает сообщение получателям события - объектам некоторых других классов. Будем называть класс, объекты которого зажигают событие, классом- отправителем сообщения (sender). Класс, чьи объекты получают сообщения, будем называть классом- получателем сообщения (receiver). Класс-отправитель сообщения, в принципе, не знает своих получателей. Объект отправляет сообщение в межмодульное пространство. Одно и то же сообщение может быть получено и по-разному обработано произвольным числом объектов разных классов. Взгляните на схему, демонстрирующую взаимодействие объектов при посылке и получении сообщения.

Взаимодействие объектов. Посылка и получение сообщения о событии

Рис. 7.1. Взаимодействие объектов. Посылка и получение сообщения о событии

Класс sender. Как объявляются события?

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

Содержательную сторону будем пояснять на содержательных примерах. А сейчас рассмотрим технический вопрос: как объявляются события средствами языка С#? Прежде всего, уточним, что такое событие с программистской точки зрения. Начнем не с самого события, а с его обработчика. Обработчик события - это метод, обычная процедура с аргументами. Какова сигнатура этого метода? Она задается событием. Как обычно в таких случаях, задается контракт между классом sender и классами reciever. Класс sender говорит своим клиентам: "У меня есть событие, оно задает сигнатуру обработчика события". Если классы receiver согласны заключить контракт с классом sender, то они должны иметь в своем составе обработчики события с заданной сигнатурой.

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

Делегаты и события

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

  • Вначале объявляется делегат - функциональный класс, задающий сигнатуру. Как отмечалось при рассмотрении делегатов, объявление делегата может быть помещено в некоторый класс, например, класс sender. Но чаще всего это объявление находится вне класса в пространстве имен. Поскольку одна и та же сигнатура может быть у разных событий, для них достаточно иметь одного делегата. Для некоторых событий можно использовать стандартные делегаты, встроенные в каркас. Тогда достаточно знать только их имена.
  • Если делегат определен, то в классе sender, создающем события, достаточно объявить событие как экземпляр соответствующего делегата. Это делается точно так же, как и при объявлении функциональных экземпляров делегата. Исключением является добавление служебного слова event. Формальный синтаксис объявления таков:
    [атрибуты] [модификаторы]event [тип, заданный делегатом] [имя события]

Есть еще одна форма объявления, но о ней чуть позже. Чаще всего атрибуты не задаются, а работа происходит с модификатором доступа - public. Приведу пример объявления делегата и события, представляющего экземпляр этого делегата:

namespace Events
{   
  public delegate void FireEvent(int day, int building);
   public class TownWithEvents
   {
      public event FireEvent fireEvent;
      …   
   }//TownWithEvents
      …
}//namespace Events

В этом примере в классе TownWithEvents введено событие. Делегат FireEvent описывает класс событий, сигнатура которых содержит два аргумента, характеризующих событие. Объект fireEvent в классе TownWithEvents является экземпляром класса, заданного делегатом. Поскольку он снабжен модификатором event, он является событием со всеми вытекающими отсюда последствиями. Модификатор доступа public делает событие доступным для клиентов класса, обрабатывающего это событие.

Как зажигаются события

Причины возникновения события могут быть разными. Поэтому вполне вероятно, что одно и то же событие будет зажигаться в разных методах класса в тот момент, когда возникнет одна из причин появления события. Поскольку действия по включению события могут повторяться, полезно в состав методов класса добавить защищенную процедуру, зажигающую событие. Даже если событие зажигается только в одной точке, написание такой процедуры считается признаком хорошего стиля. Этой процедуре обычно дается имя, начинающееся со слова On, после которого следует имя события. Будем называть такую процедуру On - процедурой. Она проста и состоит из вызова объявленного события, включенного в тест, который проверяет перед вызовом, есть ли хоть один обработчик события, способный принять соответствующее сообщение. Если таковых нет, то нечего включать событие. Вызов объявленного события называется зажиганием или включением события. Его можно рассматривать как посылку сообщения, как вызов метода клиента, обрабатывающего событие. Говоря более точно, сообщение посылается всем, кто готов принять это сообщение, и обрабатывать это сообщение будет не один метод, а все методы из списка вызовов, присоединившихся к событию. Такая возможность работы с делегатами обсуждалась в предыдущей лекции. Приведу пример:

/// <summary>
/// "Зажигание" события Fire (пожар)
/// </summary>
/// <param name="time">время обнаружения пожара</param>
/// <param name="build">
  /// здание, в котором произошел пожар </param>
void OnFire(int day, int buildings)
{
   if (fireEvent!=null)
      fireEvent(day, buildings);
}

Хочу обратить внимание: те, кто принимают сообщение о событии, должны заранее присоединить обработчики событий к объекту fireEvent, задающему событие. Присоединение обработчиков должно предшествовать зажиганию события. При таком нормальном ходе вещей найдется хотя бы один слушатель сообщения о событии, следовательно, fireEvent не будет равно null.

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

Давайте построим простую модель города, в котором возникает событие "пожар". В городе есть дома, и с некоторой вероятностью в каждом доме каждый день возможен пожар. Добавим поля к нашему классу TownWithEvents:

string townName;
int buildings;
int days;
double fireProbability;
Random random=new Random();

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

Приведу конструктор класса и метод-свойство, обеспечивающее доступ к одному из закрытых полей класса:

/// <summary>
  /// Конструктор, инициализирующий поля класса
  /// </summary>
  /// <param name="name">название города</param>
  /// <param name="buildings">число домов</param>
  /// <param name="days">число дней жизни города</param>
  /// <param name="fireProbability">
  /// вероятность пожара в доме в текущий день</param>
      public TownWithEvents(string name, int buildings,
      int days, double fireProbability)
      {
      townName = name;
      this.buildings = buildings;
      this.days = days;
      this.fireProbability = fireProbability;
      }
/// <summary>
  /// Доступ к полю townName
  /// </summary>
  public string TownName
  { get { return townName; } }

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

/// <summary>
/// Моделирование жизни города
/// </summary>
public void TownLife()
  {
        const string OK =
            "В городе все спокойно! Пожаров не было.";
        bool wasFire = false;         
     for(int day = 1; day < days; day++)
            for(int building = 1; building < buildings; building++)
                if (random.NextDouble() < fireProbability)
            {                   
               OnFire(day,building);
               wasFire = true;
            }         
        if (!wasFire)
            Console.WriteLine(OK);            
  }

Мы разобрались в том, как создаются события в классе sender. Давайте теперь рассмотрим классы, принимающие событие.

< Лекция 7 || Лекция 8: 12345 || Лекция 9 >
Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дарья Федотова
Дарья Федотова
Михаил Алексеев
Михаил Алексеев
Россия, Уфа, УГАТУ, 2002
Олег Корсак
Олег Корсак
Латвия, Рига