Этап 3. Определение метода, ответственного
за уведомление зарегистрированных
объектов о событии
В соответствии с соглашением в классе должен быть виртуальный защищенный
метод, вызываемый из кода класса и его потомков при возникновении события.
Этот метод принимает один параметр, объект
MailMsgEventArgs
, содержащий до-
полнительные сведения о событии. Реализация по умолчанию этого метода просто
проверяет, есть ли объекты, зарегистрировавшиеся для получения уведомления о
событии, и при положительном результате проверки сообщает зарегистрированным
методам о возникновении события. Вот как выглядит этот метод в нашем классе
MailManager
:
internal class MailManager {
...
// Этап 3. Определение метода, ответственного за уведомление
// зарегистрированных объектов о событии
// Если этот класс изолированный, нужно сделать метод закрытым
// или невиртуальным
protected virtual void OnNewMail(NewMailEventArgs e) {
// Сохранить ссылку на делегата во временной переменной
// для обеспечения безопасности потоков
EventHandler 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 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 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 (this TEventArgs e,
Object sender, ref EventHandler eventDelegate) {
// Копирование ссылки на поле делегата во временное поле
// для безопасности в отношении потоков
EventHandler 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
базового типа, в результате зарегистрированный объект
получает уведомление. Однако производный тип может и отказаться от пересылки
уведомления о событии.
Do'stlaringiz bilan baham: |