Глава 11. События
В этой главе рассматривается последняя разновидность членов, которые можно
определить в типе, — события. Тип, в котором определено событие (или экземпля-
ры этого типа), может уведомлять другие объекты о некоторых особых ситуациях,
которые могут случиться. Например, если в классе Button (кнопка) определить
событие Click (щелчок), то в приложение можно использовать объекты, которые
будут получать уведомление о щелчке объекта Button, а получив такое уведомле-
ние — исполнять некоторые действия. События — это члены типа, обеспечивающие
такого рода взаимодействие. А именно определения события в типе означает, что
тип поддерживает следующие возможности:
‰‰ регистрация своей заинтересованности в событии;
‰‰ отмена регистрации своей заинтересованности в событии;
‰‰ оповещение зарегистрированных методов о произошедшем событии.
Типы могут предоставлять эту функциональность при определении событий,
так как они поддерживают список зарегистрированных методов. Когда событие
происходит, тип уведомляет об этом все зарегистрированные методы.
Модель событий CLR основана на делегатах (delegate). Делегаты обеспечива-
ют реализацию механизма обратного вызова, безопасную по отношению к типам.
Методы обратного вызова (callback methods) позволяют объекту получать уведом-
ления, на которые он подписался. В этой главе мы будем постоянно пользоваться
делегатами, но их детальный разбор отложим до главы 17.
Чтобы помочь вам в полной мере разобраться в работе событий в CLR, я нач-
ну с примера ситуации, в которой могут пригодиться события. Допустим, мы
проектируем почтовое приложение. Получив сообщение по электронной почте,
пользователь может захотеть переслать его на факс или пейджер. Допустим, вы
начали проектирование приложения с разработки типа MailManager, получающе-
го входящие сообщения. Тип MailManager будет поддерживать событие NewMail.
Другие типы (например, Fax или Pager) могут регистрироваться для получения
уведомлений об этом событии. Когда тип MailManager получит новое сообщение,
он инициирует событие, в результате чего сообщение будет получено всеми заре-
гистрированными объектами. Далее каждый объект обрабатывает сообщение так,
как считает нужным.
Пусть во время инициализации приложения создается только один экземпляр
MailManager и любое число объектов Fax и Pager. На рис. 11.1 показано, как ини-
циализируется приложение и что происходит при получении сообщения.
Разработка типа, поддерживающего событие 287
Рис. 11.1. Архитектура приложения, в котором используются события
При инициализации приложения создается экземпляр объекта MailManager,
поддерживающего событие NewMail. Во время создания объекты Fax и Pager ре-
гистрируются в качестве получателей уведомлений о событии NewMail (приход
нового сообщения) объекта MailManager, в результате MailManager «знает», что
эти объекты следует уведомить о появлении нового сообщения. Если в дальнейшем
MailManager получит новое сообщение, это приведет к вызову события NewMail,
позволяющего всем зарегистрировавшимся объектам выполнить требуемую об-
работку нового сообщения.
Разработка типа,
поддерживающего событие
Для создания типа, поддерживающего одно или более событий, разработчик должен
выполнить ряд действий. Все эти действия будут описаны ниже. Наше приложение
MailManager (его можно загрузить в разделе Books сайта http://wintellect.com) со-
держит весь необходимый код типов MailManager, Fax и Pager. Как вы заметите,
типы Fax и Pager практически идентичны.
288 Глава 11. События
Этап 1. Определение типа для хранения
всей дополнительной информации,
передаваемой получателям
уведомления о событии
При возникновении события объект, в котором оно возникло, должен передать до-
полнительную информацию объектам-получателям уведомления о событии. Для
предоставления получателям эту информацию нужно инкапсулировать в собствен-
ный класс, содержащий набор закрытых полей и набор открытых неизменяемых
(только для чтения) свойств. В соответствии с соглашением, классы, содержащие
информацию о событиях, передаваемую обработчику события, должны наследовать
от типа System.EventArgs, а имя типа должно заканчиваться словом EventArgs.
В этом примере у типа NewMailEventArgs есть поля, идентифицирующие отправи-
теля сообщения (m_from), его получателя (m_to) и тему (m_subject).
// Этап 1. Определение типа для хранения информации,
// которая передается получателям уведомления о событии
internal class NewMailEventArgs : EventArgs {
private readonly String m_from, m_to, m_subject;
public NewMailEventArgs(String from, String to, String subject) {
m_from = from; m_to = to; m_subject = subject;
}
public String From { get { return m_from; } }
public String To { get { return m_to; } }
public String Subject { get { return m_subject; } }
}
ПримечАние
Тип EventArgs определяется в библиотеке классов . NET Framework Class Library (FCL)
и выглядит примерно следующим образом:
[ComVisible(true), Serializable]
public class EventArgs {
public static readonly EventArgs Empty = new EventArgs();
public EventArgs() { }
}
Как видите, в этом классе нет ничего особенного. Он просто служит базовым типом,
от которого можно порождать другие типы. С большинством событий не передается
дополнительной информации. Например, в случае уведомления объектом Button
о щелчке на кнопке, само обращение к методу обратного вызова — и есть вся нуж-
ная информация. Определяя событие, не передающее дополнительные данные,
можно не создавать новый объект Event-Args, достаточно просто воспользоваться
свойством EventArgs.Empty.
Разработка типа, поддерживающего событие 289
Этап 2. Определение члена-события
В C# событие объявляется с ключевым словом event. Каждому члену-событию
назначаются область действия (практически всегда он открытый, поэтому доступен
из любого кода), тип делегата, указывающий на прототип вызываемого метода (или
методов), и имя (любой допустимый идентификатор). Вот как выглядит член-
событие нашего класса NewMail:
internal class MailManager {
// Этап 2. Определение члена-события
public event EventHandler<NewMailEventArgs> NewMail;
...
}
Здесь NewMail — имя события, а типом события является EventHandler <New-
MailEventArgs>. Это означает, что получатели уведомления о событии должны
предоставлять метод обратного вызова, прототип которого соответствует типу-
делегату EventHandler<NewMailEventArgs>. Так как обобщенный делегат System.
EventHandler определен следующим образом:
public delegate void EventHandler<TEventArgs>
(Object sender, TEventArgs e) where TEventArgs: EventArgs;
Поэтому прототип метода должен выглядеть так:
void MethodName(Object sender, NewMailEventArgs e);
ПримечАние
Многих удивляет, почему механизм событий требует, чтобы параметр sender имел
тип Object. Вообще-то, поскольку MailManager — единственный тип, реализующий
события с объектом NewMailEventArgs, было бы разумнее использовать следующий
прототип метода обратного вызова:
void MethodName(MailManager sender, NewMailEventArgs e);
Причиной того, что параметр sender имеет тип Object, является наследование. Что произойдет, если MailManager задействовать в качестве базового класса для
создания класса SmtpMailManager? В методе обратного вызова придется в про-
тотипе задать параметр sender как SmtpMailManager, а не MailManager, но этого
делать нельзя, так как тип SmtpMailManager просто наследует событие NewMail. Поэтому код, ожидающий от SmtpMailManager информацию о событии, все равно
будет вынужден приводить аргумент sender к типу SmtpMailManager. Иначе говоря,
приведение все равно необходимо, поэтому параметр sender с таким же успехом
можно объявить с типом Object.
Еще одна причина для объявления sender с типом Object — гибкость, поскольку
делегат может применяться несколькими типами, которые поддерживают событие,
передающее объект NewMailEventArgs. В частности, класс PopMailManager мог бы
использовать делегат, даже если бы не наследовал от класса MailManager.
290 Глава 11. События
И еще одно: механизм событий требует, чтобы в имени делегата и методе обратного
вызова параметр, производный от EventArgs, назывался «e». Такое требование уста-
навливается по единственной причине: для обеспечения единообразия, облегчающе-
го и упрощающего изучение и реализацию событий разработчиками. Инструменты
создания кода (например, такой как Microsoft Visual Studio) также «знают», что нужно
вызывать параметр e.
И последнее: механизм событий требует, чтобы все обработчики возвращали void. Это обязательно, потому что при возникновении события могут выполняться не-
сколько методов обратного вызова и невозможно получить у них все возвращаемое
значение. Тип void просто запрещает методам возвращать какое бы то ни было
значение. К сожалению, в библиотеке FCL есть обработчики событий, в частности
ResolveEventHandler, в которых Microsoft не следует собственным правилам и воз-
вращает объект типа Assembly.
Этап 3. Определение метода, ответственного
за уведомление зарегистрированных
объектов о событии
В соответствии с соглашением в классе должен быть виртуальный защищенный
метод, вызываемый из кода класса и его потомков при возникновении события.
Этот метод принимает один параметр, объект MailMsgEventArgs, содержащий до-
полнительные сведения о событии. Реализация по умолчанию этого метода просто
проверяет, есть ли объекты, зарегистрировавшиеся для получения уведомления о
событии, и при положительном результате проверки сообщает зарегистрированным
методам о возникновении события. Вот как выглядит этот метод в нашем классе
MailManager:
internal class MailManager {
...
// Этап 3. Определение метода, ответственного за уведомление
// зарегистрированных объектов о событии
// Если этот класс изолированный, нужно сделать метод закрытым
// или невиртуальным
protected virtual void OnNewMail(NewMailEventArgs e) {
// Сохранить ссылку на делегата во временной переменной
// для обеспечения безопасности потоков
EventHandler<NewMailEventArgs> temp = Volatile.Read (ref NewMail);
// Если есть объекты, зарегистрированные для получения
// уведомления о событии, уведомляем их
if (temp != null) temp(this, e);
}
...
}
Разработка типа, поддерживающего событие 291
Уведомление о событии, безопасное в отношении потоков
В первом выпуске .NET Framework рекомендовалось уведомлять о событиях сле-
дующим образом:
// Версия 1
protected virtual void OnNewMail(NewMailEventArgs e) {
if (NewMail != null) NewMail(this, e);
}
Однако в методе OnNewMail кроется одна потенциальная проблема. Программ-
ный поток видит, что значение NewMail не равно null, однако перед вызовом NewMail
другой поток может удалить делегата из цепочки, присвоив NewMail значение null.
В результате будет выдано исключение NullReferenceException. Для предотвра-
щения состояния гонки многие разработчики пишут следующий код:
// Версия 2
protected void OnNewMail(NewMailEventArgs e) {
EventHandler<NewMailEventArgs> temp = NewMail;
if (temp != null) temp(this, e);
}
Идея заключается в том, что ссылка на NewMail копируется во временную
переменную temp, которая ссылается на цепочку делегатов в момент назначе-
ния. Этот метод сравнивает temp с null и вызывает temp, поэтому уже не имеет
значения, поменял ли другой поток NewMail после назначения temp. Вспомните,
что делегаты неизменяемы, поэтому теоретически этот способ работает. Однако
многие разработчики не осознают, что компилятор может оптимизировать этот
программный код, удалив переменную temp. В этом случае обе представленные
версии кода окажутся идентичными, в результате опять-таки возможно исключение
NullReferenceException.
Для реального решения этой проблемы необходимо переписать OnNewMail так:
// Версия 3
protected void OnNewMail(NewMailEventArgs e) {
EventHandler<NewMailEventArgs> temp = Thread.VolatileRead(ref NewMail);
if (temp != null) temp(this, e);
}
Вызов VolatileRead заставляет считывать NewMail в точке вызова и именно
в этот момент копировать ссылку в переменную temp. Затем вызов temp осущест-
вляется лишь в том случае, если переменная не равна null. За дополнительной
информацией о методе Volatile.Read обращайтесь к главе 28.
И хотя последняя версия этого программного кода является наилучшей и тех-
нически корректной, вы также можете использовать версию 2 с JIT-компилятором,
не опасаясь за последствия, так как он не будет оптимизировать программный код.
Все JIT-компиляторы Microsoft соблюдают принцип отказа от лишних операций
чтения из кучи, а следовательно, кэширование ссылки в локальной переменной
гарантирует, что обращение по ссылке будет производиться всего один раз. Такое
поведение официально не документировано и теоретически может измениться,
292 Глава 11. События
поэтому лучше все же использовать последнюю версию представленного про-
граммного кода. На практике Microsoft никогда не станет вводить в JIT-компилятор
изменения, которые нарушат работу слишком многих приложений1. Кроме того, со-
бытия в основном используются в однопоточных сценариях (приложения Windows
Presentation Foundation и Windows Store), так что безопасность потоков вообще
не создает особых проблем.
Для удобства можно определить метод расширения (см. главу 8), инкапсули-
рующий логику, безопасную в отношении потоков. Определите расширенный метод
следующим образом:
public static class EventArgExtensions {
public static void Raise<TEventArgs>(this TEventArgs e,
Object sender, ref EventHandler<TEventArgs> eventDelegate) {
// Копирование ссылки на поле делегата во временное поле
// для безопасности в отношении потоков
EventHandler<TEventArgs> temp = Volatile.Read(ref eventDelegate);
// Если зарегистрированный метод заинтересован в событии, уведомите его
if (temp != null) temp(sender, e);
}
}
Теперь можно переписать метод OnNewMail следующим образом:
protected virtual void OnNewMail(NewMailEventArgs e) {
e.Raise(this, ref m_NewMail);
}
Тип, производный от MailManager, может свободно переопределять метод
OnNewMail, что позволяет производному типу контролировать срабатывание со-
бытия. Таким образом, производный тип может обрабатывать новые сообщения
любым способом по собственному усмотрению. Обычно производный тип вы-
зывает метод OnNewMail базового типа, в результате зарегистрированный объект
получает уведомление. Однако производный тип может и отказаться от пересылки
уведомления о событии.
Этап 4. Определение метода, преобразующего входную
информацию в желаемое событие
У класса должен быть метод, принимающий некоторую входную информацию и в от-
вет генерирующий событие. В примере с типом MailManager метод SimulateNewMail
вызывается для оповещения о получении нового сообщения в MailManager:
internal class MailManager {
// Этап 4. Определение метода, преобразующего входную
1 Меня в этом заверил участник группы разработки JIT-компилятора.
Реализация событий компилятором 293
// информацию в желаемое событие
public void SimulateNewMail(String from, String to, String subject) {
// Создать объект для хранения информации, которую
// нужно передать получателям уведомления
NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
// Вызвать виртуальный метод, уведомляющий объект о событии
// Если ни один из производных типов не переопределяет этот метод,
// объект уведомит всех зарегистрированных получателей уведомления
OnNewMail(e);
}
}
Метод SimulateNewMail принимает информацию о сообщении и создает новый
объект NewMailEventArgs, передавая его конструктору данные сообщения. Затем
вызывается OnNewMail — собственный виртуальный метод объекта MailManager,
чтобы формально уведомить объект MailManager о новом почтовом сообщении.
Обычно это вызывает инициирование события, в результате уведомляются все за-
регистрированные объекты. (Как уже отмечалось, тип, производный от MailManager,
может переопределять это действие.)
Реализация событий компилятором
Теперь, когда вы умеете определять классы с событиями, можно поближе позна-
комиться с самим событием и узнать, как оно работает. В классе MailManager есть
строчка кода, определяющая сам член-событие:
public event EventHandler<NewMailEventArgs> NewMail;
При компиляции этой строки компилятор превращает ее в следующие три
конструкции:
// 1. ЗАКРЫТОЕ поле делегата, инициализированное значением null
private EventHandler<NewMailEventArgs> NewMail = null;
// 2. ОТКРЫТЫЙ метод add_Xxx (где Xxx – это имя события)
// Позволяет объектам регистрироваться для получения уведомлений о событии
public void add_NewMail(EventHandler<NewMailEventArgs> value) {
// Цикл и вызов CompareExchange – хитроумный способ добавления
// делегата способом, безопасным в отношении потоков
EventHandler<NewMailEventArgs>prevHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do {
prevHandler = newMail;
EventHandler<NewMailEventArgs> newHandler =
(EventHandler<NewMailEventArgs>) Delegate.Combine(prevHandler, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(
ref this.NewMail, newHandler, prevHandler);
продолжение 
294 Глава 11. События
} while (newMail != prevHandler);
}
// 3. ОТКРЫТЫЙ метод remove_Xxx (где Xxx – это имя события)
// Позволяет объектам отменять регистрацию в качестве
// получателей уведомлений о cобытии
public void remove_NewMail(EventHandler<NewMailEventArgs> value) {
// Цикл и вызов CompareExchange – хитроумный способ
// удаления делегата способом, безопасным в отношении потоков
EventHandler<NewMailEventArgs> prevHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do {
prevHandler = newMail;
EventHandler<NewMailEventArgs> newHandler =
(EventHandler<NewMailEventArgs>) Delegate.Remove(prevHandler, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(
ref this.NewMail, newHandler, prevHandler);
} while (newMail != prevHandler);
}
Первая конструкция — просто поле соответствующего типа делегата. Оно со-
держит ссылку на заголовок списка делегатов, которые будут уведомляться о воз-
никновении события. Поле инициализируется значением null; это означает, что
нет получателей, зарегистрировавшихся на уведомления о событии. Когда метод
регистрирует получателя уведомления, в это поле заносится ссылка на экземпляр
делегата EventHandler<NewMailEventArgs>, который может, в свою очередь, ссы-
латься на дополнительные делегаты EventHandler<NewMailEventArgs>. Когда полу-
чатель регистрируется для получения уведомления о событии, он просто добавляет
в список экземпляр типа делегата. Конечно, отказ от регистрации реализуется
удалением соответствующего делегата.
Обратите внимание: поле делегата (NewMail в нашем примере) всегда закрытое,
несмотря на то что исходная строка кода определяет событие как открытое. Это
делается для предотвращения некорректных операций из кода, не относящегося
к определяющему классу. Если бы поле было открытым, любой код мог бы изме-
нить значение поля, в том числе удалить все делегаты, подписавшиеся на событие.
Вторая конструкция, генерируемая компилятором C#, — метод, позволяющий
другим объектам регистрироваться в качестве получателей уведомления о со-
бытии. Компилятор C# автоматически присваивает этой функции имя, добавляя
приставку add_ к имени события (NewMail). Компилятор C# также автоматически
генерирует код метода, который всегда вызывает статический метод Combine типа
System.Delegate. Метод Combine добавляет в список делегатов новый экземпляр
и возвращает новый заголовок списка, который снова сохраняется в поле.
Третья и последняя конструкция, генерируемая компилятором C#, — метод,
позволяющий объекту отказаться от подписки на событие. И этой функции ком-
пилятор C# присваивает имя автоматически, добавляя приставку remove_ к имени
события (NewMail). Код метода всегда вызывает метод Remove типа System.Delegate.
Создание типа, отслеживающего событие 295
Последний метод удаляет делегата из списка и возвращает новый заголовок списка,
который сохраняется в поле.
Внимание
При попытке удаления метода, который не был добавлен, метод Delegate.Remove
не делает ничего. Вы не получите ни исключения, ни предупреждения, а коллекция
методов событий останется без изменений.
ПримечАние
Оба метода — add и remove — используют хорошо известный паттерн обновления
значения способом, безопасным в отношении потоков. Этот паттерн описывается
в главе 28.
В приведенном примере методы add и remove объявлены открытыми, посколь-
ку в соответствующей строке исходного кода событие изначально объявлено как
открытое. Если бы оно было объявлено как защищенное, то методы add и remove,
сгенерированные компилятором, тоже были бы объявлены как защищенные. Так
что когда в типе определяется событие, модификатор доступа события указывает,
какой код способен регистрироваться и отменять регистрацию для уведомления
о событии, но прямым доступом к полю делегата обладает только сам тип. Члены-
события также могут объявляться статическими и виртуальными; в этом случае
сгенерированные компилятором методы add и remove также будут статическими
или виртуальными соответственно.
Помимо генерирования этих трех конструкций, компиляторы генерируют запись
с определением события и помещают ее в метаданные управляемого модуля. Эта
запись содержит ряд флагов и базовый тип-делегат, а также ссылки на методы до-
ступа add и remove. Эта информация нужна просто для того, чтобы очертить связь
между абстрактным понятием «событие» и его методами доступа. Эти метаданные
могут использовать компиляторы и другие инструменты, и, конечно же, эти сведения
можно получить при помощи класса System.Reflection.EventInfo. Однако сама
среда CLR эти метаданные не использует и во время выполнения требует лишь
наличия методов доступа.
Создание типа, отслеживающего событие
Самое трудное позади. В этом разделе я покажу, как определить тип, использую-
щий событие, поддерживаемое другим типом. Начнем с изучения исходного кода
типа Fax:
internal sealed class Fax {
// Передаем конструктору объект MailManager
продолжение 
296 Глава 11. События
public Fax(MailManager mm) {
// Создаем экземпляр делегата EventHandler<NewMailEventArgs>,
// ссылающийся на метод обратного вызова FaxMsg
// Регистрируем обратный вызов для события NewMail объекта MailManager
mm.NewMail += FaxMsg;
}
// MailManager вызывает этот метод для уведомления
// объекта Fax о прибытии нового почтового сообщения
private void FaxMsg(Object sender, NewMailEventArgs e) {
// 'sender' используется для взаимодействия с объектом MailManager,
// если потребуется передать ему какую-то информацию
// 'e' определяет дополнительную информацию о событии,
// которую пожелает предоставить MailManager
// Обычно расположенный здесь код отправляет сообщение по факсу
// Тестовая реализация выводит информацию на консоль
Console.WriteLine("Faxing mail message:");
Console.WriteLine(" From={0}, To={1}, Subject={2}",
e.From, e.To, e.Subject);
}
// Этот метод может выполняться для отмены регистрации объекта Fax
// в качестве получтеля уведомлений о событии NewMail
public void Unregister(MailManager mm) {
// Отменить регистрацию на уведомление о событии NewMail объекта
MailManager. mm.NewMail -= FaxMsg;
}
}
При инициализации почтовое приложение сначала создает объект MailManager
и сохраняет ссылку на него в переменной. Затем оно создает объект Fax, передавая
ссылку на MailManager как параметр. В конструкторе Fax объект Fax регистрируется
на уведомления о событии NewMail объекта MailManager при помощи оператора +=
языка C#:
mm.NewMail += FaxMsg;
Обладая встроенной поддержкой событий, компилятор C# транслирует опе-
ратор += в код, регистрирующий объект для получения уведомлений о событии:
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));
Как видите, компилятор C# генерирует код, конструирующий делегата
EventHandler<NewMailEventArgs>, который инкапсулирует метод FaxMsg класса
Fax. Затем компилятор C# вызывает метод add_NewMail объекта MailManager, пере-
давая ему нового делегата. Конечно, вы можете убедиться в этом, скомпилировав
код и затем изучив IL-код с помощью такого инструмента, как утилита ILDasm.exe.
Создание типа, отслеживающего событие 297
Даже используя язык, не поддерживающий события напрямую, можно зареги-
стрировать делегат для уведомления о событии, явно вызвав метод доступа add.
Результат не изменяется, только исходный текст получается не столь элегантным.
Именно метод add регистрирует делегата для уведомления о событии, добавляя
его в список делегатов данного события.
Когда срабатывает событие объекта MailManager, вызывается метод FaxMsg объ-
екта Fax. Этому методу в первом параметре sender передается ссылка на объект
MailManager. Чаще всего этот параметр игнорируется, но он может и использовать-
ся, если в ответ на уведомление о событии объект Fax пожелает получить доступ
к полям или методам объекта MailManager. Второй параметр — ссылка на объект
NewMailEventArgs. Этот объект содержит всю дополнительную информацию, кото-
рая, по мнению NewMailEventArgs, может быть полезной для получателей события.
При помощи объекта NewMailEventArgs метод FaxMsg может без труда получить
доступ к сведениям об отправителе и получателе сообщения, его теме и собственно
тексту. Реальный объект Fax отправлял бы эти сведения адресату, но в данном при-
мере они просто выводятся на консоль.
Когда объекту больше не нужны уведомления о событиях, он должен отменить
свою регистрацию. Например, объект Fax отменит свою регистрацию в качестве
получателя уведомления о событии NewMail, если пользователю больше не нужно
пересылать сообщения электронной почты по факсу. Пока объект зарегистриро-
ван в качестве получателя уведомления о событии другого объекта, он не будет
уничтожен уборщиком мусора. Если в вашем типе реализован метод Dispose объ-
екта IDisposable, уничтожение объекта должно вызвать отмену его регистрации
в качестве получателя уведомлений обо всех событиях (об объекте IDisposable
см. также главу 21).
Код, иллюстрирующий отмену регистрации, показан в исходном тексте метода
Unregister объекта Fax. Код этого метода фактически идентичен конструктору
типа Fax. Единственное отличие в том, что здесь вместо += использован оператор
–=. Обнаружив код, отменяющий регистрацию делегата при помощи оператора –=,
компилятор C# генерирует вызов метода remove этого события:
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg));
Как и в случае оператора +=, даже при использовании языка, не поддерживаю-
щего события напрямую, можно отменить регистрацию делегата явным вызовом
метода доступа remove, который отменяет регистрацию делегата путем сканирования
списка в поисках делегата-оболочки метода, соответствующего переданному. Если
совпадение обнаружено, делегат удаляется из списка делегатов события. Если нет,
то список делегатов события остается, а ошибка не происходит.
Кстати, C# требует, чтобы для добавления и удаления делегатов из списка в ва-
ших программах использовались операторы += и –=. Если попытаться напрямую
обратиться к методам add или remove, компилятор C# сгенерирует сообщение об
ошибке (CS0571: оператор или метод доступа нельзя вызывать явно):
CS0571: cannot explicitly call operator or accessor
298 Глава 11. События
Явное управление регистрацией событий
В типе System.Windows.Forms.Control определено около 70 событий. Если тип
Control реализует события, позволяя компилятору явно генерировать методы
доступа add и remove и поля-делегаты, то каждый объект Control будет иметь
70 полей-делегатов для каждого события! Так как многих программистов интересует
относительно небольшое подмножество событий, для каждого объекта, созданного
из производного от Control типа, огромный объем памяти будет расходоваться
напрасно. Кстати, типы System.Web.UI.Control (из ASP.NET) и System.Windows.
UIElement (из Windows Presentation Foundation, WPF) также предлагают множество
событий, которые большинство программистов не использует.
В этом разделе рассказано о том, каким образом компилятор C# позволяет
разработчикам реализовывать события, управляя тем, как методы add и remove
манипулируют делегатами обратных вызовов. Я покажу, как явная реализация со-
бытия помогает эффективно реализовать класс с поддержкой множества событий.
Впрочем, явная реализация событий типа может оказаться полезной и в других
ситуациях.
Для эффективного хранения делегатов событий каждый объект, применяющий
события, поддерживает коллекцию (обычно это словарь), в которой идентификатор
события является ключом, а список делегатов — значением. При создании ново-
го объекта коллекция пуста. При регистрации события идентификатор события
ищется в коллекции. Если идентификатор события будет найден, то новый делегат
добавляется в список делегатов для этого события. Если идентификатор события
не найден, то он добавляется к делегатам.
При инициировании события идентификатор события ищется в коллекции.
Если в коллекции нет соответствующего элемента, то событие не регистрируется,
а делегаты не вызываются. Если же идентификатор события находится в коллекции,
то вызываются делегаты из списка, ассоциированного с этим идентификатором со-
бытия. За реализацию этого паттерна отвечает разработчик, который проектирует
тип, определяющий события. Разработчик, использующий тип, обычно не имеет
представления о внутренней реализации событий.
Приведу пример возможной реализации этого паттерна. Я реализовал класс
EventSet, представляющий коллекцию событий и список делегатов каждого со-
бытия следующим образом:
using System;
using System.Collections.Generic;
using System.Threading;
// Этот класс нужен для поддержания безопасности типа
// и кода при использовании EventSet
public sealed class EventKey : Object { }
public sealed class EventSet {
// Закрытый словарь служит для отображения EventKey -> Delegate
Явное управление регистрацией событий 299
private readonly Dictionary<EventKey, Delegate> m_events =
newDictionary<EventKey, Delegate>();
// Добавление отображения EventKey -> Delegate, если его не существует
// И компоновка делегата с существующим ключом EventKey
public void Add(EventKey eventKey, Delegate handler) {
Monitor.Enter(m_events);
Delegate d;
m_events.TryGetValue(eventKey, out d);
m_events[eventKey] = Delegate.Combine(d, handler);
Monitor.Exit(m_events);
}
// Удаление делегата из EventKey (если он существует)
// и разрыв связи EventKey -> Delegate при удалении
// последнего делегата
public void Remove(EventKey eventKey, Delegate handler) {
Monitor.Enter(m_events);
// Вызов TryGetValue предотвращает выдачу исключения
// при попытке удаления делегата с отсутствующим ключом EventKey.
Delegate d;
if (m_events.TryGetValue(eventKey, out d)) {
d = Delegate.Remove(d, handler);
// Если делегат остается, то установить новый ключ EventKey,
// иначе – удалить EventKey
if (d != null) m_events[eventKey] = d;
else m_events.Remove(eventKey);
}
Monitor.Exit(m_events);
}
// Информирование о событии для обозначенного ключа EventKey
public void Raise(EventKey eventKey, Object sender, EventArgs e) {
// Не выдавать исключение при отсутствии ключа EventKey
Delegate d;
Monitor.Enter(m_events);
m_events.TryGetValue(eventKey, out d);
Monitor.Exit(m_events);
if (d != null) {
// Из-за того что словарь может содержать несколько разных типов
// делегатов, невозможно создать вызов делегата, безопасный по
// отношению к типу, во время компиляции. Я вызываю метод
// DynamicInvoke типа System.Delegate, передавая ему параметры метода
// обратного вызова в виде массива объектов. DynamicInvoke будет
// контролировать безопасность типов параметров для вызываемого
// метода обратного вызова. Если будет найдено несоответствие типов,
// выдается исключение.
d.DynamicInvoke(newObject[] { sender, e });
}
}
300 Глава 11. События
Далее приведен пример класса, использующего класс EventSet. Этот класс
имеет поле, ссылающееся на объект EventSet, и каждое событие из этого класса
реализуется явно таким образом, что каждый метод add сохраняет заданного де-
легата обратного вызова в объекте EventSet, а каждый метод remove уничтожает
заданного делегата обратного вызова (если найдет его).
using System;
// Определение типа, унаследованного от EventArgs для этого события
public class FooEventArgs : EventArgs { }
public class TypeWithLotsOfEvents {
// Определение закрытого экземплярного поля, ссылающегося на коллекцию.
// Коллекция управляет множеством пар "Event/Delegate"
// Примечание: Тип EventSet не входит в FCL,
// это мой собственный тип
private readonly EventSet m_eventSet = newEventSet();
// Защищенное свойство позволяет производным типам работать с коллекцией
protected EventSet EventSet { get { return m_eventSet; } }
#region Code to support the Foo event (repeat this pattern for additional events)
// Определение членов, необходимых для события Foo.
// 2a. Создайте статический, доступный только для чтения объект
// для идентификации события.
// Каждый объект имеет свой хеш-код для нахождения связанного списка
// делегатов события в коллекции.
protected static readonly EventKey s_fooEventKey = newEventKey();
// 2b. Определение для события методов доступа для добавления
// или удаления делегата из коллекции.
public event EventHandler<FooEventArgs> Foo {
add { m_eventSet.Add(s_fooEventKey, value); }
remove { m_eventSet.Remove(s_fooEventKey, value); }
}
// 2c. Определение защищенного виртуального метода On для этого события.
protected virtual void OnFoo(FooEventArgs e) {
m_eventSet.Raise(s_fooEventKey, this, e);
}
// 2d. Определение метода, преобразующего входные данные этого события
public void SimulateFoo() {OnFoo(newFooEventArgs());}
#endregion
}
Программный код, использующий тип TypeWithLotsOfEvents, не может сказать,
было ли событие реализовано неявно компилятором или явно разработчиком. Он
просто регистрирует события с использованием обычного синтаксиса. Пример
программного кода:
Явное управление регистрацией событий 301
public sealed class Program {
public static void Main() {
TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents();
// Добавление обратного вызова
twle.Foo += HandleFooEvent;
// Проверяем работоспособность
twle.SimulateFoo();
}
private static void HandleFooEvent(object sender, FooEventArgs e) {
Console.WriteLine("Handling Foo Event here...");
}
}