Глава 11. События
П редмет этой главы - последний вид членов, которые можно определить
в типе, - события. Если в типе определен член-событие, то этот тип (или его
экземпляр) может уведомлять другие объекты о некоторых особых ситуациях,
которые могут случиться. Например, если в классе Button (кнопка) определить
событие Cl i ck ( щелчок), то в приложение можно использовать объекты, которые
будут получать уведомление о щелчке объекта Button, а получив такое
уведомление - исполнять некоторые действия. События - это члены типа,
обеспечивающие такого рода взаимодействие. Тип, в котором определены события,
как минимум поддерживает:
О регистрацию статического метода типа или экземплярнога метода объекта,
заинтересованного в получении уведомления о событии;
О отмену регистрации статического метода типа или экземплярнога метода
объекта, получающего уведомления о событии;
О уведомление зарегистрированных методов о том, что событие произошло.
Типы могут предоставлять эту функциональность при определении событий,
так как они поддерживают список зарегистрированных методов. Когда событие
происходит, тип уведомляет об этом все зарегистрированные методы.
Модель событий CLR основана на делегатах (delegate ). Делегаты позволяют
обращаться к методам обратного вызова, не нарушая безопасности типов.
Метод обратного вызова ( callback method ) - это механизм , позволяющий
объекту получать уведомления, на которые он подписался. В этой главе мы
будем постоянно пользоваться делегатами, но их детальный разбор отложим
до главы 1 7 .
Чтобы помочь вам досконально разобраться в работе событий в CLR, я начну
с примера ситуации, в которой могут быть полезны события. Допустим,
нам нужно создать почтовое приложение. Получив сообщение по электронной
почте, пользователь может изъявить желание переслать его по факсу или
переправить на пейджер. Допустим, вы начали проектирование приложения
с разработки типа М а i l M a n a g e r , получающего входящие сообщения. Тип
Ma i l M a n a g e r будет поддерживать событие NewMa i l . Другие типы (например, Fax
или P a g e r ) могут зарегистрироваться для получения уведомления об этом
событии. Когда тип Ма i l M a n a g e r получит новое сообщение, возникнет событие,
в результате чего сообщение будет передано всем зарегистрированным
объектам. Далее каждый объект обрабатывает сообщение в соответствии
с собственной логикой.
Разработка типа, поддерживающего событие 273
Пусть во время инициализации приложения создается только один экземпляр
Ma i 1 Manager и любое число объектов Fax и Pager. На рис. 1 1 . 1 показано, как
инициализируется приложение и что происходит при получении сообщения.
1. Объект Fax регмстрируется для nолучения уеедомлениА о событии объекта Ma11Manager.
2. Объект Pager реrмстрируется для nолучения уеедомлениА о событии объекта Ma11Manager.
З. Ma11Manager nолучает новое nочтовое сообщение.
4. Ma11Manage r уеедомляет есе зарегистрированные у него объекты, которые
обрабатывают nришедшее сообщение, как им нужно.
Рис. 1 1 . 1 . Архитектура приложения, в котором испол ьзуются события
Это приложение работает следующим образом. При его инициализации
создается экземпляр объекта M a i 1 Manager, поддерживающего событие N ewMa i 1 .
Во время создания объекты Fax и Pager регистрируются в качестве получателей
уведомлений о событии NewMa i 1 (приход нового сообщения) объекта Ма i 1 Manager,
в резу ль тате М а i 1 Manager <<знает􀍨, что эти объекты следует уведомить о наличии
нового сообщения. Если в дальнейшем Mai 1 Manager получит новое сообщение,
это приведет к вызову события NewMa i 1 , позволяющего всем зарегистрировавшимся
объектам выполнить требуемую обработку нового сообщения.
Разработка типа,
поддерживающего событие
Создание типа, поддерживающего одно и л и более событий, требует о т разработчика
пройти несколько этапов. В этом разделе я расскажу о каждом из них.
Наше приложение Ma i 1 Manager ( его можно скачать с сайта http://wintellect.com)
содержит весь необходимый код типов Ma i 1 Manager, Fax и Pager. Как вы заметите,
типы Fax и Pager практически идентичны.
274 Глава 1 1 . События
Этап 1 . Определение типа для хранения всей
дополн ительной информации , передаваемой
получателя м уведомления о событии
При возникновении события объект, в котором оно возникло, должен передать
дополнительную информацию объектам-получателям уведомления о событии.
Для предоставления получателям эту информацию нужно инкапсулировать
в собственный класс, содержащий набор закрытых полей и набор открытых
неизменяемых (только для чтения) свойств. В соответствии с соглашением,
классы, содержащие информацию о событиях, передаваемую обработчику события,
должны наследовать от типа System . EventArg s , а имя типа должно заканчиваться
словом EventArg s . В этом примере у типа NewMa i 1 EventArgs есть поля,
идентифицирующие отправителя сообщения (m_f rom), его получателя (m_to)
и тему (m_s ubject).
1 1 Этап 1 . Определ е н и е типа для хра н е н и я и нфор ма ц и и .
1 1 котора я передается получ а т ел я м у в едомлен и я о собы т и и
i nt e rn a 1 c 1 a s s NewMa i 1 EventArgs : EventArgs {
p r i vate readon 1 y St r i n g m_from . m_to . m_s ubj ect :
puЬl i c NewMa i 1 EventArgs ( St r i ng from . St ri ng to . St ri ng s ubj ect )
m_f rom = from : m_to = t o : m_s ubj ect = s ubj ect :
puЬl i c St ri ng F rom { get { ret u rn m_from : } }
puЬl i c S t r i ng То { get { ret u rn m_to : } }
puЬl i c St r i ng Subj ect { get { return m_s ubj ect : } }
П Р И М ЕЧАНИ Е
Ти п EventArgs определяется в библиотеке классов . N ET Framework Class
Library ( FCL) и выглядит примерно следующим образом:
[ ComV i s i Ь l e ( t rue ) ]
[ S e r i а 1 i z а Ы е ]
p u Ы i c c 1 a s s EventArgs {
puЬl i c sta t i c reado n 1 y EventArgs Empty = new EventArgs ( ) :
puЫ i c EventArgs ( ) { }
Как в идите, в нем нет ничего особенного. Он п росто служит базовым типом,
от которого можно пораждать другие типы. С б ольшинством событий не
передается доп олн ител ьной и нформаци и . Например, в случае уведомления
объектом Button о щелчке на кнопке, обращение к методу обратно го вызова
- и есть вся нужная и нформация. Определяя событие, не передающее
дополн ител ьные дан н ы е , можно не создавать новый объект Event-Args, достаточ
н о п росто воспол ьзоваться свойством EventArgs . Em pty.
Разработка типа, поддерживающего событие 275
Этап 2. Определение члена-события
В С# член-событие объявляется с ключевым словом event. Каждому членусобытию
назначаются область действия ( практически всегда он открытый,
поэтому доступен из любого кода) , тип делегата, указывающий на прототип
вызываемого метода (или методов), и имя (любой допустимый идентификатор).
Вот как выглядит член-событие нашего класса NewMa i l :
i ntern a l c l a s s M a i l Manager {
1 1 Этап 2 . Определение член а - собы т и я
puЫ i c event EventHand l e r<NewMa i l EventArgs> NewMa i l :
Здесь NewMa i l - имя события, а типом члена-события является EventHa n d l e r
<NewM a i l E v e n t A r g s > . Э т о означает, что получатели уведомления о событии
должны предоставлять метод обратного вызова, прототип которого совпадает
с типом-делегатом EventHandl e r<NewMa i l EventArgs>. Обобщенный делегат System .
EventHandl er определен следующим образом:
puЬl i c del egate voi d EventHandl e r<TEventArgs>
( Object sende r . TEventArgs е) where TEventArg s : EventArg s :
Поэтому прототип метода должен выглядеть так:
voi d MethodName ( Object sende r . NewMa i l EventArgs е ) :
П Р И М ЕЧАНИ Е
Многих удивляет, почему механизм событий требует, чтобы параметр seпder
имел тип Object. Вообще-то, поскольку MaiiManager - еди нствен н ы й т и п ,
реализующий события с объектом N ewM a i i EventArgs , было бы разумнее
использовать следующий прототип метода обратного вызова:
voi d MethodName ( Ma i l Manager sende r . NewMa i l EventArgs е ) ;
Причиной того, что параметр sender имеет тип Object, является наследование.
Что произойдет, есл и Maii Manager задействовать в качестве базового
класса для создания класса SmtpMaiiManager? В методе обратного вызова
придется в прототи пе задать параметр sender как SmtpMaiiManager, а не
MaiiManager, но этого делать нельзя , так как тип SmtpMaiiManager просто
наследует событие N ewMail . Поэтому код, ожидающий от SmtpMaiiManager
информацию о событ и и , все равно будет вынужден п р и водить аргумент
sender к типу Smtp MaiiManager. И наче говоря, п р и веде н и е все равно необходимо,
поэтому п роще всего сделать так, чтобы параметр sender и м ел
тип Object.
Еще одна причина того, что sender относят к типу Object - гибкость, поскольку
делегат может применяться несколькими типа м и , которые поддерживают
событие, передающее объект NewMai i EventArgs . В частности, класс
276 Глава 1 1 . События
PopMaiiM anager мог бы использовать делегат, даже если бы не наследовал
от класса Maii Manager.
И еще одно: механизм событий требует, чтобы в имени делегата и методе
обратного вызова производн ы й от EventArg s параметр назы вался «е».
Единственная п р и ч и н а - обеспечить еди нообразие, облегчая и упрощая
для разработчи ков изучение и реализацию событий . Инструменты создания
кода (например, такой как Microsoft Visual Studio) также «знают», что нужно
вызы вать параметр е.
И последнее, механизм событий требует, чтобы все обработч ики возвращал
и voi d . Это обязател ьно, потому что при возн икновен и и события могут
выполняться несколько методов обратного вызова и невозможно получить
у них все возв ращаемое знач е н и е . Ти п void п росто запрещает методам
возвращать какое бы то ни было значение. К сожалению, в библиотеке FCL
есть обработч ики событи й , в частности Resolve - Eve ntHandler, в которых
M i c rosoft н е сл едует собстве н н ы м п равилам и возвращает объект типа
AssemЫy.
Этап 3. Определение метода , ответственного
за уведомление зарегистрированных объектов
о событии
В соответствии с соглашением в классе должен быть виртуальный защищенный
метод, вызываемый из кода класса и его потомков при возникновении события.
Этот метод принимает один параметр, объект Ma i 1 M s g E ventArgs , содержащий
дополнительные сведения о событии. Реализация по умолчанию этого метода
просто проверяет, есть ли объекты, зарегистрировавшиеся для получения
уведомления о событии, и при положительном результате проверки сообщает
зарегистрированным методам о возникновении события. Вот как выглядит этот
метод в нашем классе Ма i 1 Manager:
i nt e r n a 1 c 1 a s s M a i 1 Manager {
1 1 Э т а п 3 . Оnредел е н и е метода . о т в е тс т в е н н о г о з а уведомление
11 з а ре г истриро в а н ных объектов о собы т и и
1 1 Е с л и э т о т к л а с с и з ол и ро в а н н ый . нуж но сдел а т ь метод з а крытым
11 и л и н е в и ртуал ь н ы м
p rotected v i rtua 1 voi d OnNewMa i 1 ( NewMa i 1 EventArgs е ) {
1 1 Сохра н и т ь nоле деле г а т а во временном поле
11 для обес печ е н и я без о п а с н о с т и потоков
EventHa nd1 e r<NewMa i 1 EventArgs> temp = NewMa i 1 :
1 1 Есл и ест ь объекты . з а ре г истр иро в а н н ые д л я получ е н и я
1 1 у в едомлен и я о событ и и . уведомл я е м и х
i f ( temp ! = n u 1 1 ) temp ( t h i s . е ) :
Разработка типа, поддерживающего событие 277
Поднятие события безопасным в отношении потоков образом
В первой поставке .NET Framework рекомендуемым способом поднятия события
был такой:
11 Верс и я 1
p rotected v i rtua l voi d OпNewMa i l ( NewMa i l EveпtArgs е ) {
i f ( NewMa i l ! = п u l l ) NewMa i l ( t h i s . е ) ;
Проблема метода OпNewMa i l состоит в следующем. Поток может видеть, что
значение NewMa i l не равно пu l l , однако перед вызовом NewMa i l другой поток может
удалить делегата из цепочки, присвоив NewMa i l значение п u l l . В результате
будет вброшено исключение N u l l RefereпceExcepti о п . Для решения проблемы
многие разработчики пишут следующий код:
11 Верс и я 2
p rotected voi d OпNewMa i l ( NewMa i l EveпtArgs е ) {
EveпtHaпdl e r<NewMa i l EveпtArgs> temp = NewMa i l ;
i f ( temp ! = п u l l ) temp ( th i s . е ) ;
Идея здесь в том, что ссылка на NewMa i l копируется во временную переменную
temp, которая ссылается на цепочку делегатов в момент назначения. Этот
метод сравнивает temp со значением п u l l и вызывает temp, поэтому уже не имеет
значения, поменял ли другой поток NewMa i l после назначения temp. Вспомните,
что делегаты неизменяемы, и именно поэтому этот способ работает. Однако
многие разработчики не знают, что компилятор может оптимизировать этот
программный код, удалив переменную temp. В этом случае обе представленные
версии кода окажутся идентичными, в результате опять-таки возможно исключение
Nu l l RefereпceExcept i оп.
Для реального решения этой проблемы необходимо переписать OпNewMa i l так:
1 1 Верс и я 3
protected voi d OпNewMa i l ( NewMa i l EveпtArgs е ) {
EveпtHa пdl e r<NewMa i l EveпtArgs> temp = Thread . Vo l a t i l eRea d ( ref NewMa i l ) ;
i f ( temp ! = п u l l ) temp ( t h i s . е ) ;
Вызов Vol a t i l eRead заставляет считывать NewMa i l в точке вызова и именно
в этот момент копировать ссылку в переменную temp. Затем переменная temp
вызывается лишь тогда, когда она не равна п u l l . К несчастью, из-за отсутствия
перегруженной версии обобщенного метода Vo l at i l eRead невозможно записать
программный код в этом виде. Однако существует обобщенная перегруженная
версия I пterl ocked . Compa reExcha пge, которую вы можете использовать:
11 Верс и я 4
p rotected voi d OпNewMa i l ( NewMa i l EveпtArgs е )
EveпtHaпdl e r<NewMa i l EveпtArgs> temp =
I пterl ocked . Compa reExchaпge ( ref NewMa i l . пu l l . п u l l ) ;
i f ( temp ! = п u l l ) temp ( t h i s . е ) ;
продолжение .Р
278 Глава 1 1 . События
Здесь Compa reExc h a nge изменяет ссылку temp на n u l l , если значение NewMa i l
равно n u l l , и не трогает ее, если NewMa i l не равно n u l l . Другими словами,
Compa reExch a nge не изменяет значение NewMa i l вообще, но возвращает значение
внутри NewMa i l безопасным в отношении потоков образом. Для более детального
изучения методов T h r e a d . V o l a t i l eRead и I nt e r l ocked . Compa reExc h a n g e см. главу
28.
И хотя версия 4 этого программнаго кода является наилучшей и технически
корректной, вы также можете использовать версию 2 с ]IT -компилятором, не
опасаясь за последствия, так как он не будет оптимизировать программный
код. Однако этого нет в официальной документации, то есть гипотетически он
может внести в код свои изменения, поэтому все же лучше воспользоваться
версией 4 представленного программнога кода.
Важно отметить, что из-за конкуренции потоков возможна ситуация, когда
метод вызывается уже после того, как он удален из цепочки делегатов события.
Для удобства вы можете определить расширенный метод (см. главу 8 ) ,
инкапсу лирующий безопасную в отношении потоков логику. Определите расширенный
метод следующим образом:
puЫ i c stat i c c l a s s EventArgExten s i ons {
puЫ i c stat i c v o i d Rai s e<TEventArgs> ( th i s TEventArgs е .
Obj ect sende r . ref EventHand l e r<TEventArgs > eventDe l egate )
where TEventArgs : EventArgs {
1 1 К о п и ро в а н и е ссыл к и н а поле деле г а та во временное поле
11 для безопасности в отноше н и и потоков
Event Hand l e r<TEventArgs> temp =
I nt e r l ocked . Compa reExc h a nge ( ref eventDe l egate . n u l l . n u l l ) ;
1 1 Если з а ре г истриро в а н н ы й метод з а и н тересо в а н в событи и . уведомите е г о
i f ( temp ! = n u l l ) temp ( sende r . е ) ;
Теперь можно переписать метод OnNewMa i l следующим образом:
protected v i rt u a l voi d OnNewMa i l ( NewMa i l EventArgs е) {
e . Ra i s e ( th i s . ref m_NewMa i l ) ;
Тип, производный от Ma i l Ma n a g e r , может свободно переопределять метод
OnNewMa i l , что позволяет производиому типу контролировать срабатывание события.
Таким образом, производный тип может обрабатывать новые сообщения
любым способом по собственному усмотрению. Обычно производный тип вызывает
метод OnNewMa i l базового типа, в результате зарегистрированный объект
получает уведомление. Однако производный тип может и отказаться от пересылки
уведомления о событии.
Как реал изуются события 279
Этап 4. Определение метода , транслирующего
входную информацию в желаемое событие
У класса должен быть метод, принимающий некоторую входную информацию
и в ответ генерирующий событие. В примере с типом M a i 1 Manager метод
S i mu 1 ateNewMa i 1 вызывается для оповещения о получении нового сообщения
в Ма i 1 Manager:
i ntern a 1 c 1 a s s M a i 1 Manager {
1 1 Этап 4 . Определение метода . транслирующе г о в ход ную
11 и нфор м а ц и ю в желаемое событие
puЬl i c voi d Si mu1 ateNewMa i 1 ( St r i ng from . St r i ng to . St r i ng s ubj ect )
1 1 Соз д а т ь объект д л я хра н е н и я и нфор ма ц и и . которую
11 нужно перед а т ь получ а т ел я м уведомле н и я
NewMa i 1 EventArgs е = new NewMa i 1 EventArg s ( from . to . s ubj ect ) :
1 1 Выз в а т ь в и ртуал ь н ы й метод . у ведомляющий объект о собы т и и
1 1 Есл и н и о д и н и з п ро и з водных т и п о в н е переоп редел я е т э тот метод .
1 1 объект у в е д о м и т всех з а ре г истриро в а н н ы х п олуч а телей у в едомле н и я
OnNewMa i 1 ( е ) :
Метод S i mu 1 ateNewMa i 1 принимает информацию о сообщении и создает новый
объект NewMa i 1 EventArg s , передавая его конструктору данные сообщения. Затем
вызывается OnNewMa i 1 - собственный виртуальный метод объекта M a i 1 Manager,
чтобы формально уведомить объект M a i 1 Ma n a g e r о новом почтовом сообщении.
Обычно это вызывает генерацию события, в результате уведомляются
все зарегистрированные объекты. ( Как уже отмечалось, тип, производный от
Ma i 1 Manager, может переопределять это действие.)
Как реал изуются события
Научившись определять класс с членом-событием, можно поближе познакомиться
с самим событием и узнать, как оно работает. В классе M a i 1 Manager есть
строчка кода, определяющая сам член-событие:
puЫ i c event EventHa nd1 e r<NewMa i 1 EventArgs> NewMa i 1 :
При компиляции этой строки компилятор превращает ее в следующие три
конструкции:
11 1 . ЗАКРЫТОЕ поле деле г а т а . и н и ц и а л и з и ро в а н ное з н а ч е нием n u 1 1
pri vate EventHand1 e r<NewMa i 1 EventArgs> NewMa i 1 = n u 1 1 :
продолжение .Р
280 Глава 1 1 . События
11 2 . ОТКРЫТЫЙ метод add_Xxx ( г д е Ххх - это и м я собы т и я )
1 1 Поз в ол я ет объ е к т а м ре г истриро в а т ь с я д л я получ е н и я уведомлен и й о соб ы т и и
puЫ i c voi d add_NewMa i l ( Event Hand l e r<NewMa i l EventArgs> v a l ue ) {
1 1 Цикл и в ы з о в Compa reExcha nge - фа н т а с т и ч ес к и й с п особ доба влен и я
1 1 деле г а т а в собы т и е безопасным в отношен и и потоков путем
EventHand l e r<NewMa i l E ventArgs>prevHa ndl e r ;
EventHand l e r<NewMa i l EventArgs> newMa i l = t h i s . NewMa i l ;
do {
prevHand l e r = newMa i l ;
EventHand l e r<NewMa i l EventArgs>newH a nd l e r =
< EventHand l e r<NewMa i l EventArgs> ) Del egate . Combi ne ( p revHandl e r . va l ue ) ;
newMa i l = I nt e r l ocked . Compa reExcha nge<EventHandl e r<NewMa i l EventArgs>> (
ref t h i s . NewM a i l . n ewH a nd l e r . p revHa ndl e r ) ;
wh i l e ( newMa i l ! = prevHandl e r ) ;
1 1 3 . ОТКРЫТЫЙ метод remove_Xxx ( г де Ххх - это и м я собы т и я )
1 1 Поз в ол я е т объ е к т а м отмен я т ь ре г истра ц и ю в качестве
11 п олуч а телей у в едомлен и й о собы т и и
puЫ i c voi d remove NewMa i l ( EventHand l e r<NewMa i l EventArgs> v a l ue )
1 1 Цикл и в ы з о в Compa reExcha nge - ф а н т а с т и ч е с к и й сп особ
11 удален и я деле г а т а из собы т и я без о п а сным в отношен и и потоков путем
EventHand l e r<NewMa i l EventArgs> p revHa nd l e r ;
EventHand l e r<NewMa i l EventArgs> newMa i l = t h i s . NewMa i l ;
do {
prevHand l e r = newMa i l ;
EventHand l e r<NewMa i l EventArgs> newHa n d l e r =
( EventHand l e r<NewMa i l EventArg s > ) Del egate . Remov e ( prevHa ndl e r . v a l u e ) ;
newMa i l = I nt e r l ocked . Compa reExcha nge<Event Hand l e r<NewMa i l EventArgs>> (
ref t h i s . NewMa i l . newH a nd l e r . prevHa ndl e r ) ;
} wh i l e ( newMa i l ! = prevHandl e r ) ;
Первая конструкция - просто поле соответствующего типа делегата. Оно
содержит ссылку на заголовок списка делегатов, которые будут уведомляться
при возникновении события. Поле инициализируется значением nu l l ; это означает,
что нет получателей, ожидающих уведомления о событии. Когда метод
регистрирует получателя уведомления, это поле начинает ссьшаться на экземпляр
делегата EventHandl e r<NewM a i l EventArgs>, который может, в свою очередь,
ссылаться на дополнительные делегаты EventHa n d l e r<NewMa i l EventArgs>. Когда
получатель регистрируется для получения уведомления о событии, он просто
добавляет в список экземпляр типа делегата. Ясно, что отказ от регистрации
означает удаление соответствующего делегата.
О братите внимание: в примере поле делегата, NewMa i l , всегда закрытое,
несмотря на то что исходная строка кода определяет событие как открытое.
Причина - необходимость предотвратить доступ из кода, не относящегося
к определяющему классу. Если бы поле было открытым, любой код мог бы
Как реал изуются события 281
изменить значение поля, в том числе удалить все делегаты, подписавшиеся на
событие.
Вторая создаваемая компилятором С# конструкция - метод, позволяющий
другим объектам регистрироваться в качестве получателей уведомления
о событии. Компилятор С# автоматически присваивает этой функции имя,
добавляя приставку add_ к имени события (NewMa i l ). Компилятор С# также
автоматически генерирует код метода, который всегда вызывает статический
метод Combi ne типа System . De l egate. Метод Combi ne добавляет в список делегатов
новый экземпляр и возвращает новый заголовок списка, который снова сохраняется
в поле.
Третья и последняя создаваемая компилятором С# конструкция представляет
собой метод, позволяющий объекту отказаться от подписки на событие.
И этой функции компилятор С# присваивает имя автоматически, добавляя
приставку remove_ к имени события (NewM a i l ). Код метода всегда вызывает метод
Remove типа System . Del egate. Последний метод удаляет делегат из списка и
возвращает новый заголовок списка, который сохраняется в поле.
В Н И МАН И Е
Есл и в ы попробуете удал ить метод , кото р ы й н и когда не добавлял ся , то
метод Delegate . Remove н ичего не сделает. Не п о я вится ни исключе н и я ,
н и предуп режде н и я , а коллекция методов событий останется б е з изме нений.
П Р И М ЕЧАНИ Е
Оба метода - add и remove - используют хорошо известн ы й эталон для
модификации значения защи ще н н ы м в отношении потоков способо м . Этот
эталон описывается в главе 28.
В этом примере методы add и remove являются открытыми, поскольку в соответствующей
строке исходного кода событие изначально объявлено как
открытое. Если бы оно было объявлено как закрытое, то методы add и remove,
сгенерированные компилятором, тоже были бы объявлены как закрытые. Так
что когда в типе определяется событие, модификатор доступа события указывает,
какой код способен регистрироваться и отменять регистрацию для уведомления
о событии, но прямым доступом к полю делегата обладает только сам
тип. Члены-события также могут объявляться статическими и виртуальными;
в этом случае сгенерированные компилятором методы add и remove также будут
статическими или виртуальными соответственно.
Помимо генерации этих трех конструкций, компиляторы генерируют запись
с определением события и помещают ее в метаданные управляемого модуля.
Эта запись содержит ряд флагов и базовый тип-делегат, а также ссылки на
методы-аксессоры add и remove. Эта информация нужна просто для того, чтобы
282 Глава 1 1 . События
очертить связь м ежду абстрактным попятнем 4 событие􀍨 и его методамиаксессорами.
Эти метаданные могут использовать компиляторы и другие инструменты,
и , конечно же, эти сведения можно получить при помощи класса
System . Refl ect i on . Е vent I n fo. Однако сама среда CLR эти метаданные не использует
и во время выполнения требует лишь методы-аксессоры.
Создание типа ,
отслеживающего событие
Ну, самое трудное позади. В этом разделе я показываю, как определить тип,
использующий событие, поддерживаемое другим типом. Начнем с изучения
исходного текста типа Fax:
i ntern a l sea l ed c l a s s Fax {
1 1 Передаем констру к тору объект Ma i l Manager
puЬl i c Fax ( Ma i l Manager mm ) {
1 1 Создаем э к з е м п л я р дел е г а т а EventHand l e r<NewMa i l EventArgs> .
1 1 ссыл а ющ и й с я н а метод обра т н о г о в ы з о в а FaxMsg
11 Ре г истрируем обра т н ы й в ы з о в для собы т и я NewMa i l объекта Ma i l Manager
mm . NewMa i l += FaxMsg :
}
1 1 Mai l Manager в ы з ы в а е т э т о т метод для у в едомлен и я
1 1 объекта F a x о п р и б ы т и и н о в о г о поч т о в о г о сообще н и я
p r i vate voi d FaxMs g ( Obj ect sende r . NewMa i l EventArgs е )
1 1 ' s ender ' можно и с п ол ь з о в а т ь д л я в з а и модейст в и я с объектом Ma i l Manage r .
1 1 есл и нуж но верну т ь ему к а кую - то и нформ а ц ию
1 1 ' е ' ука з ы в ает допол н и тел ь ную и н форм а ц и ю о событ и и .
1 1 которую п ожелает предоста в и т ь Ma i l Manager
11 Обыч н о распол оже н н ы й з десь код отпра вляет сообще н и е по фа ксу
1 1 В тест о в о м в а р и а н т е про г р а м м ы этот метод
11 в ы в о д и т и нфор м а ц и ю н а консол ь
Consol e . W r i teli n e ( " Fa x i ng ma i l mes s a ge : " ) :
Consol e . W r i teli n e ( " F rom= { O } . To= { l } . Subj ect= { 2 } " .
e . F rom . е . То . e . Subject ) ;
1 1 Этот метод м ожет в ы п ол н я т ьс я д л я о т м е н ы ре г истра ц и и объекта Fax
11 в к а ч ес т в е п олуч теля у в едомле н и й о собы т и и NewMa i l
puЬl i c voi d Unreg i ster ( Ma i l Manager mm ) {
Созда ние типа, отслеживающего событие 283
11 Отме н и т ь ре г истрацию на уведомление о собы т и и NewMa i 1 объекта
Mai 1 Manage r . mm . NewMa i 1 - = FaxMsg :
}
При инициализации почтовое приложение сначала создает объект Mai 1 Manager
и сохраняет ссылку на него в переменной. Затем оно создает объект F a x, передавая
ссылку на Ma i 1 Manager как параметр. После создания делегата объект F a x
регистрируется при помощи оператора += языка С# для уведомления о событии
NewMa i 1 объекта М а i 1 Manager:
mm . NewMa i 1 += F a xMsg :
Обладая встроенной поддержкой событий, компилятор С # транслирует
оператор += в код, регистрирующий объект для получения уведомлений о событии:
mm . add_NewMa i 1 ( new EventHand 1 e r<NewMa i 1 EventArgs> ( t h i s . F a xMsg ) ) :
Как видите, ком пилятор С # создает код, конструиру ю щ и й делегат
EventHa nd1 e r<NewMa i 1 EventArgs>, который инкапсулирует метод NewMa i 1 класса Fax.
Затем компилятор С# вызывает метод add_NewMa i 1 объекта Mai 1 Manager, передавая
ему новый делегат. Ясно, что вы можете убедиться в этом, скомпилировав код
и затем изучив IL-код с помощью такого инструмента, как утилита I LDasm .exe.
Даже используя язык, не поддерживающий события напрямую, можно зарегистрировать
делегат для уведомления о событии, явно вызвав метод-аксессор
add. Результат идентичен, только исходный текст получается не столь изящным.
Именно метод add, регистрирующий делегат для уведомления о событии, добавляет
делегат в список делегатов данного события.
Когда срабатывает событие объекта Ma i 1 M a n a g e r , вызывается метод F a xMsg
объекта Fax. Этому методу передается ссылка на объект Ma i 1 Manager в качестве
первого параметра, или sender. Чаще всего этот параметр игнорируется, но он
может и использоваться, если в ответ на уведомление о событии объект F а х
пожелает получить доступ к полям и л и методам объекта M a i 1 M a n a g e r . Второй
параметр - это ссылка на объект NewMa i 1 EventArg s . Этот объект содержит всю
дополнительную информацию, которая, по мнению NewMa i 1 EventArgs , может быть
полезной для получателей события.
При помощи объекта NewМai 1 EventArgs метод FaxМsg может без труда получить
доступ к сведениям об отправителе и получателе сообщения, его теме и собственно
тексту. Реальный объект Fax отправлял бы эти сведения адресату, но
в данном примере они просто выводятся на консоль.
Когда объекту больше не нужны уведомления о событиях, он должен отменить
свою регистрацию. Например, объект F a x отменит свою регистрацию
в качестве получателя уведомления о событии NewMa i 1 , если пользователю
больше не нужно пересылать сообщения электронной почты по факсу. Пока
объект зарегистрирован в качестве получателя уведомления о событии другого
284 Глава 1 1 . События
объекта, он не может попасть в сферу действия сборщика мусора. Если в вашем
типе реализован метод Di spose объекта I D i sposaЫ е, уничтожение объекта
должно вызвать отмену его регистрации в качестве получателя уведомлений
обо всех событиях (об объекте I Di s po s a Ы e см. также главу 2 1 ).
Код, иллюстрирующий отмену регистрации, показан в исходном тексте
метода Un reg i s t e r объекта Fax. Код этого метода фактически идентичен конструктору
типа Fax. Единственное отличие в том, что здесь вместо += использован
оператор -=. Обнаружив код, отменяющий регистрацию делегата при
помощи оператора -=, компилятор С# генерирует вызов метода remove этого
события:
mm . remove_NewMa i l ( new EventHand l e r<NewMa i l EventArg s > ( FaxMsg ) ) :
Как и в случае оператора +=, даже при использовании языка, не поддерживающего
события напрямую, можно отменить регистрацию делегата, явно
вызывая метод-аксессор remove, который отменяет регистрацию делегата путем
сканирования списка в поисках делегата-оболочки метода, соответствующего
переданному методу обратного вызова. Если обнаружено совпадение, делегат
удаляется из списка делегатов события. Если нет, ошибка не возникает, и список
делегатов события остается неизменным.
Кстати, С # требует, чтобы для добавления и удаления делегатов из списка
в ваших программах использовались операторы += и - = . Если попытаться
напрямую обратиться к м етодам add или remo ve, компилятор С# сгенерирует
сообщение об ошибке ( C S057 1 : оператор или аксессор нельзя вызывать
явно):
CS057 1 : c a nnot expl i ci t l y c a l l operator or accessor
Я вное уп равление
регистрацией событи й
В типе Sys t em . W i ndows . Fo rms . Cont rol определено около 7 0 событий. Если тип
Cont rol реализует события, позволяя компилятору явно генерировать методы
аксессора add и remove и поля-делегаты, то каждый объект Cont rol будет иметь
70 полей-делегатов для каждого события ! Многие программисты оперируют
несколькими событиями, следовательно, для каждого объекта, созданного из
унаследованного от Cont rol типа, потребуется огромный объем памяти. Кстати,
типы System . Web . U I . Control ( из ASP.NET) и System . Wi ndows . U I El ement (из Windows
Presentation Foundation, WPF) также предлагают множество событий, которые
большинство программистов не использует.
В этом разделе рассказано о том, каким образом компилятор С# позволяет
разработчикам явно управлять регистрацией событий, контролировать добавление
и удаление методов, манипулировать делегатами обратных вызовов.
Я вное управление регистрацией событий 285
Для эффективного использования делегатов события каждый объект, применяющий
события, содержится в главной коллекции (обычно это словарь)
с несколькими типами идентификаторов событий в качестве ключей и списком
делегатов в качестве значений. При создании нового объекта эта коллекция
пуста. При регистрации события идентификатор события ищется в коллекции.
Если идентификатор события найден, то новый делегат добавляется в список
делегатов для этого события. Если идентификатор события не найден, то он
добавляется к делегатам. Когда наступает событие, идентификатор события
ищется в коллекции. Если в коллекции нет записи для него, то оно не регистрируется,
и делегаты не вызываются. Если идентификатор события находится
в коллекции, то вызывается список делегатов, ассоциированных с этим идентификатором
события.
Реализация этого эталона проектирования находится в зоне ответственности
разработчика, проектирующего типы, определяющие события. Разработчик,
использующий тип, обычно не знает о том, как события реализованы. Приведу
пример того, как вы можете усовершенствовать данный эталон. Я реализовал
класс EventSet, представляющий коллекцию событий и каждого делегата события
следующим образом:
usi ng System :
us i ng System . Co l l ecti ons . Generi c :
1 1 Этот класс нуже н д л я поддержа н и я безопасности т и п а
1 1 и кода п р и испол ь з о в а н и и EventSet
puЫ i c sea l ed c l a s s EventKey Obj ect { }
puЫ i c sea l ed c l a s s EventSet
11 За крытый сло в а р ь служ и т д л я отображе н и я EventKey -> Del egate
pri vate readon l y Di ct i on a ry<EventKey . Del egate> m_events
newDi cti ona ry<Event Key . Del egate> ( ) :
1 1 Доба вление отображе н и я EventKey - > Del egate . есл и е г о н е сущес т вует
11 И ком п о н о в к а деле г а т а с сущест вующ и м ключ о м EventKey
puЫ i c voi d Add ( EventKey eventKey . Del egate h a n d l e r ) {
Mon i tor . Ente r ( m_events ) :
Del egate d :
m_events . TryGetVa l ue ( event Key . out d ) :
m_events [ eventKey ] = Del egate . Combi ne ( d . h a n d l e r ) :
Mon i tor . Exi t ( m_events ) :
1 1 Удаление деле г а т а и з EventKey ( если о н сущест вует )
1 1 и л и к в и д а ц и я отображе н и я EventKey - > Del egate .
1 1 ко г д а удален послед н и й деле г а т
puЫ i c voi d Remove ( EventKey event Key . Del egate h a n d l e r )
продолжеuие .,Р
286 Глава 1 1 . События
Mon i t o r . Ente r ( m_events ) ;
1 1 Вы з о в T ryGetVa l ue . ч т обы исключ е н и е не было
11 вброшено п р и п о п ы тке удал и т ь деле г а т а
1 1 и з н еуста н о в л е н н о г о ключ а EventKey
Del egate d ;
i f ( m_event s . T ryGet Va l ue ( eventKey . out d ) ) {
d = Del egate . Remove ( d . handl e r ) ;
1 1 Если деле г а т остается . то уста н о в и т ь н о в ы й ключ Event Key ,
1 1 и н а ч е - у д а л и т ь EventKey
i f ( d ! = n u l l ) m_event s [ eventKey] = d ;
e l s e m_event s . Remo v e ( eventKey ) ;
}
Mon i t o r . Exi t ( m_events ) ;
1 1 Под н я т и е собы т и я д л я обоз н а ч е н н о г о ключа EventKey
puЬl i c v o i d R a i s e ( EventKey eventKey , Obj ect sende r . EventArgs е ) {
1 1 f-e вбрасы в а т ь и с кл юч е н и е . если ключ EventKey не н а з н а ч е н
Del egate d ;
Mon i t o r . Ente r ( m_events ) ;
m_event s . T ryGetVa l ue ( eventKey , out d ) ;
Mon i t o r . Exi t ( m_events ) ;
i f ( d ! = n u l l ) {
1 1 И з - з а т о г о ч то ката л о г м ожет содержа т ь нескол ь ко раз ных т и п о в
1 1 деле г а т о в . н е в о з можно соз д а т ь в ы з о в з а щи щен н о г о т и п а деле г а т а
1 1 в о в рем я ком п ил я ц и и . Я в ы з вал метод Oynami c i nvoke т и п а
1 1 System . De l egate . переда в в метод обра т н о г о в ы з о в а пара метры в в и д е
1 1 м а сс и в а объекто в . Dyn ami c i nvoke будет контрол и ро в а т ь безопасность
11 типов пара метров метода обра т н о г о вызова и вызов этого метода . Если
11 будет н а й де н о несоот ветст в и е т и п о в . вбрасывается исключ е н и е
d . Dynami c i n v ok e ( newOb j ect [ J { sender . е } ) ;
П Р И М ЕЧАНИ Е
FCL оп ределяет т и п Syste m.Com poпentMode i . EventHandlerlist, который, п о
существу, делает т о же самое, что и класс EventSet. Тип ы System .Windows .
Forms.Control и System .We b . U I .Control используют тип EventHandlerlist для
поддержания набора редких событий. Конечно, вы можете задействовать тип
EventHandlerlist из б и бл и отеки FCL. Раз н и ца между типом EventHandlerlist
и м о и м типом EventSet заключается в том , что EventHa ndlerlist при меняет
связа н н ы й список вместо хэш -табл и цы. Это означает, что доступ к элементам
, управляемый п р и помощи EventHandlerlist, медленнее, чем доступ при
помощи EventSet. К тому же EventHand l e rlist н е п редлагает безопас н ы й
в отн о ш е н и и п отоков способ д л я доступа к события м , при необходи мости
вы м ожете реал изовать собстве н н ы й безопасный в отношении потоков обработ
ч и к при помощи коллекции EventHandlerlist.
Явное управление регистрацией событий 287
Я продемонстрирую класс, который использует класс EventSet. Этот класс
имеет поле, ссылающееся на объект EventSet, и каждое событие из этого класса
реализуется явно таким образом, что каждый метод add содержит специального
делегата обратного вызова в объекте EventSet, а каждый метод remove уничтожает
специального делегата обратного вызова (если найдет его).
u s i ng Sys tem :
1 1 Определение т и п а . унаследо в а н н о г о от EventArgs для э т о г о собы т и я
puЬl i c c l a s s FooEventArgs : EventArgs { }
puЬl i c c l a s s TypeW i t h lotsOfEvents {
1 1 Определение з а крыто г о э к з е м пл я р н а г о п ол я . ссылающе гося на колл е к ц и ю
1 1 Колл е к ц и я упра в л я ет м н ожес т в о м п а р " Event / De l egate"
11 Примеч а н ие : Т и п EventSet н е я вл я ется ч а с т ь ю FCL .
1 1 это мой собс т в е н н ы й т и п
p r i vate readon l y EventSet m_eventSet = newEventSet ( ) :
1 1 Защище н н ое с в о й с т в о п о з воляет у н а следо в а т ь т и п доступа к колл е к ц и и
p rotected EventSet EventSet { g e t { ret u rn m_eventSet : } }
#reg i on Code to s u pport the Foo event (
repeat t h i s pattern fo r add i t i o n a l event s )
1 1 Определение членов необхо д и м о д л я собы т и я Foo
11 2а . Соз д а й те с т а т и ч е с к и й неи з ме н я ем ы й ( тол ь ко для ч т ен и я ) объ ект
11 для и д е нтифик а ц и и собы т и я
1 1 Кажд ый объект и м еет с в о й х э ш - код для на хожден и я с в я з а н н о г о с п иска
11 дел е г а т а собы т и я в колл е к ц и и объекта
p rotected stat i c readon l y EventKey s_fooEvent Key = newEventKey ( ) :
1 1 2d . Определ ите для собы т и я метод - а ксессор . который доба в л я е т
1 1 дел е г а т а в коллекцию и удаляет е г о и з колл е к ц и и
puЬl i c event EventHandl e r<FooEventArgs> F o o {
add { m_eventSet . Ad d ( s_fooEventKey . v a l ue ) : }
remove { m_eventSet . Remove ( s_fooEventKey , v a l ue ) :
1 1 2е . Определ ите з а щище н н ы й в и ртуал ь н ы й метод Оп д л я э т о г о соб ы т и я
p rotected v i rt u a l voi d OnFoo ( FooEventArgs е )
m_eventSet . Ra i se ( s_fooEventKey , t h i s . е ) :
1 1 2f . Определ ите метод . осущес т в л я ющ и й в в од э т о г о собы т и я
puЬl i c voi d S i mu l ateFoo ( ) { OnFoo ( newFooEventArgs ( ) ) : }
#end reg i on
}

Программный код, использующий тип ypeW i thlotsOfEvent s, не может сказать,
было ли событие реализовано неявно компилятором или явно разработчиком.
Он просто регистрирует события при помощи обычного синтаксиса. Вот программный
код, демонстрирующий это:
puЬl i c s e a l ed c l a s s P rogram {
puЫ i c s t at i c v o i d M a i n ( ) {
TypeW i t hlotsOfEvents twl e = newTypeWi thlotsOfEvent s ( ) ;
1 1 Доба в ь те обра т н ы й в ы з о в
twl e . Foo + = Handl eFooEvent ;
1 1 Про в ер ые . ч т о э т о работает
twl e . S i mu l ateFoo ( ) ;
p r i vate s t a t i c v o i d Handl e FooEvent ( ob j ect sende r . FooEventArgs е )
Con s o l e . W r i teli n e ( " Ha n d l i ng Foo Event here . . . " ) ;


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