Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ооп теория

.pdf
Скачиваний:
22
Добавлен:
14.02.2015
Размер:
4 Мб
Скачать
☆

}

// Обработчик события - выдает сообщение. //Разрешает добавление элементов, меньших 20. private void ListChanged(object sender,

ChangedEventArgs args)

{

Console.WriteLine("Receiver2: Сообщаю об изменениях:" + " Объект класса {0} : " + "Item ={1}",

sender.GetType(), args.Item); args.Permit = ((int)args.Item < 20);

}

public void OnConnect()

{

//Присоединяет обработчик к событию

List.Changed += new ChangedEventHandler(ListChanged);

//Заметьте, допустимо только присоединение (+=), //но не замена (=)

//List.Changed = new ChangedEventHandler(ListChanged);

}

public void OffConnect()

{

//Отсоединяет обработчик от события и удаляет список

List.Changed -= new ChangedEventHandler(ListChanged); List = null;

}

}//class Receiver2

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

public void TestChangeList()

{

//Создаются два объекта, вырабатывающие события

ListWithChangedEvent list = new ListWithChangedEvent(); ListWithChangedEvent list1 = new ListWithChangedEvent(); //Создаются три объекта двух классов EventReceiver1 и //Receiver2, способные обрабатывать события класса

//ListWithChangedEvent

EventReceiver1 Receiver1 = new EventReceiver1(list); Receiver2 Receiver21 = new Receiver2 (list); Receiver2 Receiver22 = new Receiver2(list1);

Random rnd = new Random();

//Работа с объектами, приводящая к появлению событий list.Add(rnd.Next(20)); list.Add(rnd.Next(20));

list[1] =17;

int val = (int)list[0] + (int)list[1];list.Add(val); list.Clear();

list1.Add(10); list1[0] = 25; list1.Clear();

//Отсоединение обработчика событий

Receiver1.OffConnect(); list.Add(21); list.Clear();

}

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

421

Рис. 21.2. События в мире объектов Две проблемы с обработчиками событий

Объекты, создающие события, ничего не знают об объектах,

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

Игнорирование коллег

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

Слово "event" играет важную роль, позволяя решить проблему, названную нами "игнорированием коллег". В чем ее суть? В том, что некоторые из классов Receiver могут вести себя некорректно по отношению к своим

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

422

обработчика события в классе Receiver можно попытаться вместо присоединения обработчика выполнить операцию присваивания, игнорируя,

тем самым, уже присоединенный список обработчиков. Взгляните еще раз на процедуру OnConnect класса Receiver2; там демонстрируется такая попытка в закомментированном операторе. Аналогично, в процедуре OffConnect вместо отсоединения (операции -) можно попытаться присвоить событию значение null, отсоединяя тем самым всех других обработчиков.

С этим как-то нужно бороться. Ключевое слово "event" дает указание компилятору создать для события закрытое поле, доступ к которому можно получить только через два автоматически создаваемых для события метода:

Add, выполняющий операцию присоединения "+=", и Remove, выполняющий обратную операцию отсоединения "-=". Никаких других операций над событиями выполнять нельзя. Тем самым, к счастью, решается проблема игнорирования коллег. Ошибки некорректного поведения класса Receiver

ловятся еще на этапе трансляции.

ПЕРЕОПРЕДЕЛЕНИЕ ЗНАЧЕНИЙ АРГУМЕНТОВ СОБЫТИЯ

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

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

Приведенный выше пример "Работа со списками" демонстрирует не самый лучший способ определения аргументов, провоцирующий классы Receiver на некорректное обращение с аргументами. Напомню, в классе

ChangedEventArgs, определяющем аргументы события, оба свойства item и

423

permit являются закрытыми. Но определены процедуры - свойства Item и Permit, реализующие полный доступ к свойствам, поскольку определены обе процедуры get и set. Это несколько облегчило задачу, поскольку позволило изменять значение входного аргумента item перед зажиганием события для передачи его обработчику. Но входной аргумент оказался незащищенным, и

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

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

В классе ChangedEventArgs следует изменить процедуру-свойство Item,

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

ChangedEventArgs с нужным значением свойства item. Приведу соответствующий код:

public object Item

{

get {return(item);} //set { item = value;}

}

public ChangedEventArgs(object item)

{

this.item = item;

}

В методы класса ListWithChangedEvent, зажигающие события, нужно ввести изменения. Теперь перед каждым вызовом нужно создавать новый объект, задающий аргументы. Вот измененный код:

public override int Add(object value)

{

int i=0;

424

ChangedEventArgs evargs = new ChangedEventArgs(value); //evargs.Item = value;

OnChanged(evargs);

if (evargs.Permit) i = base.Add(value); else

Console.WriteLine("Добавление элемента запрещено." + "Значение = {0}", value);

return i;

}

public override void Clear()

{

ChangedEventArgs evargs = new ChangedEventArgs(0); //evargs.Item=0;

OnChanged(evargs);

base.Clear();

}

public override object this[int index]

{

set

{

ChangedEventArgs evargs = new ChangedEventArgs(value); //evargs.Item = value;

OnChanged(evargs); if (evargs.Permit)

base[index] = value;

else

Console.WriteLine("Замена элемента запрещена."

+

" Значение = {0}", value);

}

get {return(base[index]);}

}

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

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

Эта проблема остается открытой, в языке C# здесь "дыра" - нет специальных средств, позволяющих избежать или, по крайней мере, предупредить о возникновении подобной ситуации. Вся ответственность лежит на

425

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

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

полученные коллегами - ранее отработавшими обработчиками события.

КЛАССЫ С БОЛЬШИМ ЧИСЛОМ СОБЫТИЙ

Как было сказано, каждое событие класса представляется полем этого класса.

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

public event <Имя Делегата> <Имя события>

{

add {...} remove {...}

}

426

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

Давайте построим небольшой пример, демонстрирующий такой способ объявления и работы с событиями. Вначале построим класс с несколькими событиями:

class ManyEvents

{

//хэш таблица для хранения делегатов

Hashtable DStore = new Hashtable(); public event EventHandler Ev1

{

add

{

DStore["Ev1"]= (EventHandler)DStore["Ev1"]+ value;

}

remove

{

DStore["Ev1"]= (EventHandler)DStore["Ev1"]- value;

}

}

public event EventHandler Ev2

{

add

{

DStore["Ev2"]= (EventHandler)DStore["Ev2"]+ value;

}

remove

{

DStore["Ev2"]= (EventHandler)DStore["Ev2"]- value;

}

}

public event EventHandler Ev3

{

add

{

DStore["Ev3"]= (EventHandler)DStore["Ev3"]+ value;

}

remove

{

DStore["Ev3"]= (EventHandler)DStore["Ev3"]- value;

}

}

public event EventHandler Ev4

{

add

{

DStore["Ev4"]= (EventHandler)DStore["Ev4"]+ value;

}

remove

427

{

DStore["Ev4"]= (EventHandler)DStore["Ev4"]- value;

}

}

public void SimulateEvs()

{

EventHandler ev = (EventHandler) DStore["Ev1"]; if(ev != null) ev(this, null);

ev = (EventHandler) DStore["Ev3"]; if(ev != null) ev(this, null);

}

}//class ManyEvents

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

классе определен также метод SimulateEvs, при вызове которого зажигаются два из четырех событий - Ev1 и Ev3.

Рассмотрим теперь класс ReceiverEvs, слушающий события. Этот класс построен по описанным ранее правилам. В нем есть ссылка на класс,

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

class ReceiverEvs

{

private ManyEvents manyEvs;

public ReceiverEvs( ManyEvents manyEvs)

{

this.manyEvs = manyEvs; OnConnect();

}

public void OnConnect()

{

manyEvs.Ev1 += new EventHandler(H1); manyEvs.Ev2 += new EventHandler(H2); manyEvs.Ev3 += new EventHandler(H3); manyEvs.Ev4 += new EventHandler(H4);

428

}

public void H1(object s, EventArgs e)

{

Console.WriteLine("Событие Ev1");

}

public void H2(object s, EventArgs e)

{

Console.WriteLine("Событие Ev2");

}

public void H3(object s, EventArgs e)

{

Console.WriteLine("Событие Ev3");

}

public void H4(object s, EventArgs e)

{

Console.WriteLine("Событие Ev4");

}

}//class ReceiverEvs

Тестирующая процедура состоит из нескольких строчек, в которых создаются нужные объекты и запускается метод Simulate, зажигающий события:

public void TestManyEvents()

{

ManyEvents me = new ManyEvents(); ReceiverEvs revs = new ReceiverEvs(me); me.SimulateEvs();

}

Все работает предусмотренным образом.

Проект "Город и его службы"

Завершить лекцию о событиях хочется содержательным учебным проектом, в

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

пожарными, милицией, скорой помощью. Конечно, все упрощено, в

реальном городе событиями являются не только пожары и преступления, но и более приятные ситуации: день города, открытие фестивалей и выставок,

строительство новых театров и институтов.

429

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

public class NewTown

 

 

 

{

 

 

 

//свойства

 

 

 

private int build,

BuildingNumber;

//дом и число домов в городе

private int day, days;

//Текущий день года

//городские службы

 

 

 

private Police policeman ;

 

 

private Ambulance ambulanceman ;

 

private FireDetect

fireman ;

 

 

//события в городе

 

 

 

public event FireEventHandler Fire;

 

//моделирование случайных событий

 

private Random rnd

= new Random();

 

//вероятность пожара в доме в текущий день: p= m/n

private int m = 3,

n= 10000;

 

 

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

//конструктор класса

public NewTown(int TownSize, int Days)

{

BuildingNumber = rnd.Next(TownSize); days = Days;

policeman = new Police(this); ambulanceman= new Ambulance(this); fireman= new FireDetect(this); policeman.On();

ambulanceman.On();

fireman.On();

}

При создании объектов этого класса задается размер города - число его домов и период времени, в течение которого будет моделироваться жизнь города. При создании объекта создаются его службы - объекты соответствующих классов Police, Ambulance, FireDetect, которым передается ссылка на сам объект "город". При создании служб вызываются методы On,

подключающие обработчики события Fire каждой из этих служб к событию.

В соответствии с ранее описанной технологией определим метод OnFire,

включающий событие:

430