Правильный способ вызывать события в.NET Framework
В настоящее время " Избегайте проверки на нулевые обработчики событий" находится в верхней части ответов на пост под названием " Скрытые возможности C#" и содержит информацию, вводящую в заблуждение.
Хотя я понимаю, что переполнение стека является "демократией", и ответ поднялся на вершину благодаря публичному голосованию, я чувствую, что многие люди, проголосовавшие за ответ, либо не имели полного понимания C#/.NET или Не нашли время, чтобы полностью понять последствия практики, описанной в посте.
Короче говоря, публикация рекомендует использовать следующую конструкцию, чтобы избежать необходимости проверять нулевое значение при вызове события.
public event EventHandler SomeEvent = delegate {};
// Later..
void DoSomething()
{
// Invoke SomeEvent without having to check for null reference
SomeEvent(this, EventArgs.Empty);
}
На первый взгляд это может показаться умным способом, но это может стать причиной некоторых серьезных головных болей в большом приложении, особенно если задействован параллелизм.
Перед вызовом делегата события вы должны проверить нулевую ссылку. Тот факт, что вы инициализировали событие с пустым делегатом, не означает, что пользователь вашего класса не установит для него значение null и не нарушит ваш код.
Нечто подобное типично:
void DoSomething()
{
if(SomeEvent != null)
SomeEvent(this, EventArgs.Empty);
}
Но даже в приведенном выше примере существует вероятность, что хотя DoSomething() может быть запущен потоком, другой может удалить обработчики событий, и может возникнуть условие гонки.
Предположим этот сценарий:
Нить А. Нить Б. ------------------------------------------------------------------------- 0: if(SomeEvent!= Null) 1: { // удалить все обработчики SomeEvent 2: SomeEvent(this, EventArgs.Empty); 3: }
Поток B удаляет обработчики событий события SomeEvent после того, как код, который вызывает событие, проверил делегат на наличие нулевой ссылки, но до того, как он вызвал делегат. Когда SomeEvent (это, EventArgs.Empty); вызов сделан, SomeEvent имеет значение null, и возникает исключение.
Чтобы избежать такой ситуации, лучший способ поднять события таков:
void DoSomething()
{
EventHandler handler = SomeEvent;
if(handler != null)
{
handler(this, EventArgs.Empty);
}
}
Для подробного обсуждения темы EventHandlers в.NET я предлагаю прочитать " Руководства по проектированию фреймворков " Кшиштофа Квалины и Брэда Абрамса, глава 5, раздел 4 - Дизайн событий. Особенно обсуждения этой темы Эриком Ганнерсоном и Джо Даффи.
Как предложил Эрик, в одном из ответов ниже я должен указать, что может быть разработано лучшее решение для синхронизации, которое решило бы проблему. Моя цель с этим постом состояла в том, чтобы повысить осведомленность и не дать единственно верное решение проблемы. Как предложено Эриком Липпертом и Эриком Ганнерсоном в вышеупомянутой книге, конкретное решение проблемы зависит от программиста, но важно то, чтобы проблема не была проигнорирована.
Надеемся, что модератор прокомментирует ответ, чтобы не подозревающие читатели не были введены в заблуждение плохим шаблоном.
6 ответов
Я поднял тот же вопрос около недели назад и пришел к противоположному выводу:
События C# и безопасность потоков
Ваше резюме не делает ничего, чтобы убедить меня в обратном!
Во-первых, клиенты класса не могут присвоить событию значение null. Вот и весь смысл event
ключевое слово. Без этого ключевого слова это было бы поле с делегатом. При этом все операции над ним являются частными, за исключением зачисления и исключения из списка.
В результате присваивание delegate {}
чтобы событие при строительстве полностью соответствовало требованиям правильной реализации источника события.
Конечно, внутри класса может быть ошибка, когда событие установлено null
, Однако в любом классе, который содержит поле любого типа, может быть ошибка, которая устанавливает для поля значение null
, Вы бы выступали за то, чтобы каждый раз при доступе к ЛЮБОМУ членскому полю класса мы писали такой код?
// field declaration:
private string customerName;
private void Foo()
{
string copyOfCustomerName = customerName;
if (copyOfCustomerName != null)
{
// Now we can use copyOfCustomerName safely...
}
}
Конечно, нет. Все программы стали бы вдвое длиннее и вдвое менее читабельными, без веской причины. Такое же безумие возникает, когда люди применяют это "решение" к событиям. События не являются общедоступными для присваивания, так же как и частные поля, и поэтому их можно использовать напрямую, если вы инициализируете их пустым делегатом при построении.
Единственная ситуация, в которой вы не можете сделать это, - это когда у вас есть событие в struct
, но это не совсем неудобство, так как события имеют тенденцию появляться на изменяемых объектах (указывая на изменение состояния) и struct
Общеизвестно, что если они мутируют, то они хитрые, поэтому лучше всего сделать их неизменяемыми, и, следовательно, события мало полезны для struct
s.
Как я описал в своем вопросе, может существовать еще одно совершенно отдельное состояние гонки: что, если клиент (приемник событий) хочет быть уверен, что его обработчик не будет вызван после его исключения из списка? Но, как отметил Эрик Липперт, ответственность за решение лежит на клиенте. Вкратце: невозможно гарантировать, что обработчик событий не будет вызван после его исключения из списка. Это неизбежное следствие неизменности делегатов. Это верно независимо от того, вовлечены ли потоки или нет.
В блоге Эрика Липперта он ссылается на мой SO вопрос, но затем ставит другой, но похожий вопрос. Я думаю, что он сделал это с законной риторической целью - просто для того, чтобы подготовить почву для обсуждения условий вторичной расы, которые влияют на организаторов события. Но, к сожалению, если вы перейдете по ссылке на мой вопрос, а затем немного небрежно прочитаете его пост в блоге, у вас может сложиться впечатление, что он отказывается от техники "пустой делегат".
Фактически он говорит: "Есть и другие способы решения этой проблемы, например, инициализация обработчика для получения пустого действия, которое никогда не удаляется", что является техникой "пустой делегат".
Он охватывает "выполнение нулевой проверки", потому что это "стандартный шаблон"; мой вопрос был, почему это стандартная модель? Джон Скит предположил, что, учитывая, что совет предшествует добавлению анонимных функций в язык, это, вероятно, просто похмелье от C# версии 1, и я думаю, что это почти наверняка так, поэтому я принял его ответ.
"То, что вы инициализировали событие с пустым делегатом, не означает, что пользователь вашего класса в какой-то момент не установит для него значение null и не нарушит ваш код".
Не может случиться События "могут появляться только слева от += или -= (кроме случаев, когда они используются внутри типа)", чтобы процитировать ошибку, которую вы получите при этом. Конечно, "кроме случаев, когда они используются внутри типа" делает это теоретической возможностью, но не той, которая была бы интересна любому здравомыслящему разработчику.
Просто для ясности. Подход, использующий пустой делегат в качестве начального значения для события, работает даже при использовании с сериализацией:
// to run in linqpad:
// - add reference to System.Runtime.Serialization.dll
// - add using directives for System.IO and System.Runtime.Serialization.Formatters.Binary
void Main()
{
var instance = new Foo();
Foo instance2;
instance.Bar += (s, e) => Console.WriteLine("Test");
var formatter = new BinaryFormatter();
using(var stream = new MemoryStream()) {
formatter.Serialize(stream, instance);
stream.Seek(0, SeekOrigin.Begin);
instance2 = (Foo)formatter.Deserialize(stream);
}
instance2.RaiseBar();
}
[Serializable]
class Foo {
public event EventHandler Bar = delegate { };
public void RaiseBar() {
Bar(this, EventArgs.Empty);
}
}
// Define other methods and classes here
Просто как примечание, http://blogs.msdn.com/ericlippert/archive/2009/04/29/events-and-races.aspx
Это постоянная ссылка на статью, на которую ссылался Эрик.
Что бы это ни стоило, вы должны действительно заглянуть в класс EventsHelper Джувала Лоуи, а не делать все самостоятельно.
Brumme - папа для Эрика и Абрамса. Вы должны читать его блог, а не проповедовать ни одному из двух публицистов. Парень серьезно техничен (в отличие от логотипов парикмахеров высокого уровня). Без "Редмонд Флауэрс на 1 ТБ земли" он даст вам правильное объяснение того, почему расы и модели памяти являются проблемой для управляемой (re: shield-the-children) среды, о чем говорил другой постер выше.
Кстати, все начинается с них, ребята из реализации C++ CLR:
blogs.msdn.com/cbrumme