Текст перед кодом, чтобы не искажать сводку вопроса.
class Tree
{
public event EventHandler MadeSound;
public void Fall() { MadeSound(this, new EventArgs()); }
static void Main(string[] args)
{
Tree oaky = new Tree();
oaky.Fall();
}
}
Я нечасто использовал события в C#, но тот факт, что это вызовет NullRefEx, кажется странным. Ссылка EventHandler считается пустой, потому что в настоящее время у нее нет подписчиков, но это не означает, что событие не произошло, не так ли?
Обработчики событий отличаются от стандартных делегатов ключевым словом мероприятие. Почему разработчики языка не настроили их молча стрелять в пустоту, когда у них нет подписчиков? (Я так понимаю, вы можете сделать это вручную, явно добавив пустой делегат).
когда дерево падает в лесу, и никто не слушает, он все равно издает звук - он говорит «Му»
Я опубликовал пример ниже, используя метод расширения, который, как мне кажется, позволяет достичь того, чего вы хотите, - изолировать все нулевые проверки в одном месте.





Что ж, каноническая форма такова:
void OnMadeSound()
{
if (MadeSound != null)
{
MadeSound(this, new EventArgs());
}
}
public void Fall() { OnMadeSound(); }
что на очень немного быстрее, чем вызов пустого делегата, поэтому скорость взяла верх над удобством программирования.
Я думаю, что рекомендуется защитить метод On *, чтобы производный класс мог реагировать на событие или изменять способ и время вызова события. Просто как дополнение.
На самом деле, самая безопасная форма такая: protected void OnMadeSound () {EventHandler tempHandler = MadeSound; если (tempHandler! = null) {tempHandler (это, новый EventArgs ()); }}
Метод должен быть защищен, а tempHandler предотвращает возможность возникновения исключения NullReferenceException, если обработчик удален между нулевой проверкой и фактическим возникновением события.
Очень дзен, а?
Вы должны проверить значение null, если хотите вызвать событие:
protected void OnMyEvent()
{
if (this.MyEvent != null) this.MyEvent(this, EventArgs.Empty);
}
Было бы неплохо, если бы вам не пришлось с этим возиться, но это перерыв.
Джеймс представил хорошее техническое обоснование, я также хотел бы добавить, что я видел, как люди использовали это преимущество: если ни один подписчик не слушает событие, они предпримут действия, чтобы зарегистрировать его в коде или что-то подобное. Простой пример, но подходящий в этом контексте.
Вам нужно понимать, что на самом деле делает ваше объявление события. Он объявляет как событие, так и переменную. Когда вы ссылаетесь на него внутри класса, вы просто ссылаетесь на переменную, которая будет иметь значение null, когда нет подписчиков.
Я не думаю, что это просто ссылка на переменную, поскольку C# не позволяет классу 1 манипулировать своими событиями так, как он манипулирует переменными. Например, C# разрешит «someEvent + = someDeletate;», но я не думаю, что он позволит «someEvent = someEvent + someDelegate;». Учитывая это, я озадачен, почему использование события способом, синтаксически похожим на вызов делегата, не кодируется как «захватить делегат и вызвать, если не null». С другой стороны, если бы у меня были мои druthers, события все равно использовали бы другой механизм, чем многоадресные делегаты.
@supercat событие C# - это просто делегат с ключевым словом event. Это предлагает особые ограничения доступа, которые вы описали, но это это просто класс.
@Gusdor: Нет, это не нет «просто делегат», равно как и свойство является «просто полем». Фактически, событие - это пара методов: один для подписки и один для отказа от подписки. В каком смысле это «просто класс»?
@JonSkeet: Вы хоть представляете, почему в C# синтаксис вызова событий не интерпретировал нулевое событие как бездействие, учитывая, что делегат нулевого события является допустимой ожидаемой ситуацией? Требование, чтобы программисты написали {var handler=SomethingHappened; if (handler!=null) handler(this,e);} или более короткий, но неправильный if (somethingHappened!=null) somethinghappened(this,e);, не очень помогает.
@supercat: Вы имеете в виду вызов делегата? Вы же не хотели бы ограничивать его событиями, подобными полям только? Но в любом случае нет, я не знаю, почему C# был разработан таким образом. Я подозреваю, что все могло бы быть сделано по-другому, если бы они начинали с нуля.
@supercat Оба ваших примера вызова событий не являются потокобезопасными и, следовательно, "неправильными" по той же причине. Ваша «лучшая» версия может привести к вызову обработчиков, отписавшихся, точно так же, как ваша «плохая» версия может привести к вызову нулевого делегата.
@Ashigore: если событие было отменено между моментом, когда процессор перешел к обработчику события, и временем, когда он выполнил его первую инструкцию, это фактически привело бы к выполнению обработчика, происходящему после отмены, но ответственность за этот случай явно будет лежать обработчик событий, а не механизм обработки событий. Если обработчик событий правильно закодирован, чтобы допускать этот случай, у него не будет проблем с любыми другими случаями, когда он вызывается после отмены подписки.
Какой смысл создавать событие, если никто не слушает? Технически, это то, как C# решил реализовать это.
В C# событие - это делегат с некоторыми особыми перьями. Делегат в этом случае можно рассматривать как связанный список указателей функций (на методы-обработчики подписчиков). Когда вы «запускаете событие», каждый указатель функции вызывается по очереди. Первоначально делегат - это нулевой объект, как и все остальное. Когда вы выполняете + = для первого действия подписки, вызывается Delegate.Combine, который создает экземпляр списка. (Вызов null.Invoke () вызывает исключение null - когда событие запускается.)
Если вы все еще чувствуете, что «этого не должно быть», используйте вспомогательный класс EventsHelper, как упоминалось здесь, со старым и улучшенным «публикацией защитных событий» http://weblogs.asp.net/rosherove/articles/DefensiveEventPublishing.aspx.
Еще один хороший способ обойти это без необходимости проверять значение null:
class Tree
{
public event EventHandler MadeSound = delegate {};
public void Fall() { MadeSound(this, new EventArgs()); }
static void Main(string[] args)
{
Tree oaky = new Tree();
oaky.Fall();
}
}
Обратите внимание на анонимный делегат - возможно, это небольшое снижение производительности, поэтому вам нужно выяснить, какой метод (проверка на null или пустой делегат) лучше всего работает в вашей ситуации.
ваш код по-прежнему может установить для события MadeSound значение null, поэтому вы все равно можете получить исключение NullReferenceException, что сделает делегат {} бессмысленным
Или вместо того, чтобы устанавливать для него значение null, вы можете установить его для делегирования {} ... Как часто вы устанавливаете событие в значение null? Мне еще не приходилось этого делать.
Спасибо за ответы. Я понимаю, почему возникает исключение NullReferenceException и как его обойти.
Gishu said
What is the point of raising an event if no one is listening?
Ну, может, дело в терминологии. Мне кажется, что привлекательность системы «событий» заключается в том, что вся ответственность за последствия произошедшего события должна лежать на наблюдателях, а не на исполнителях.
Возможно, лучше спросить: если поле делегата объявлено с ключевым словом события перед ним, почему компилятор не переводит все экземпляры:
MadeSound(this, EventArgs.Empty)
к
if (MadeSound != null) { MadeSound(this, EventArgs.Empty); }
за кулисами так же, как и другие сокращения синтаксиса? Количество шаблонных методов проверки нуля OnSomeEvent, которые люди должны писать вручную, должно быть колоссальным.
Компилятор изолирует то, что он здесь делает, в один аспект - создает переменную и событие с тем же именем. Я бы предпочел, чтобы он не делал большего, ради сложности. Однако избежать проверки на null очень просто: публичное событие EventHandler Click = delegate {};
Итак, извне класса MadeSound обращается к «событию», которое управляет добавлением / удалением подходящих делегатов к экземпляру делегата EventHandler, который имеет тот же идентификатор и скрывает «событие» внутри? Это означает, что событие не запускается напрямую, а создается список делегатов?
@Jon Я не согласен. Я очень уважаю вас, но не понимаю, почему вы отстаиваете шаблон инициализатора делегата {}.
Рекомендуемый шаблон (.net 2.0+)
public class MyClass
{
public event EventHandler<EventArgs> MyEvent; // the event
// protected to allow subclasses to override what happens when event raised.
protected virtual void OnMyEvent(object sender, EventArgs e)
{
// prevent race condition by copying reference locally
EventHandler<EventArgs> localHandler = MyEvent;
if (localHandler != null)
{
localHandler(sender, e);
}
}
public void SomethingThatGeneratesEvent()
{
OnMyEvent(this, EventArgs.Empty);
}
}
Я вижу много рекомендаций для пустого делегата {} в инициализаторе, но я полностью с этим не согласен. Если вы будете следовать приведенному выше шаблону, вы будете проверять event != null только в одном месте. Инициализатор пустого делегата {} - пустая трата времени, потому что это дополнительный вызов для каждого события, он тратит впустую память и все равно может завершиться ошибкой, если MyEvent был установлен в значение null в другом месте моего класса.
* Если ваш класс закрыт, вы бы не сделали OnMyEvent() виртуальным.
В этом случае было бы полезно использовать метод расширения.
public static class EventExtension
{
public static void RaiseEvent<T>(this EventHandler<T> handler, object obj, T args) where T : EventArgs
{
if (handler != null)
{
handler(obj, args);
}
}
}
Затем его можно использовать, как показано ниже.
public event EventHandler<YourEventArgs> YourEvent;
...
YourEvent.RaiseEvent(this, new YourEventArgs());
Спасибо. На самом деле я сам пришел к такому подходу и сделал его и для простого старого EventArgs :)
Также +1 для примера. Думаю, вызов Fall в одиноком лесу не вызовет MadeSound.