Правильный способ вызывать события в .NET Framework - PullRequest
28 голосов
/ 08 мая 2009

В настоящее время " Избегайте проверки на нулевые обработчики событий" находится вверху ответов на пост под названием Скрытые возможности 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 () может быть запущен потоком, другой может удалить обработчики событий, и может возникнуть условие гонки.

Предположим этот сценарий:

      Thread A.                           Thread B.
    -------------------------------------------------------------------------
 0: if(SomeEvent != null)
 1: {                                     // remove all handlers of 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 ]

26 голосов
/ 09 мая 2009

Я поднял ту же проблему около недели назад и пришел к противоположному выводу:

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, и я думаю, что это почти наверняка так, поэтому я принял его ответ.

15 голосов
/ 09 мая 2009

«Тот факт, что вы инициализировали событие с пустым делегатом, не означает, что пользователь вашего класса не установит для него значение null и не нарушит ваш код.»

Не может быть. События «могут появляться только слева от + = или - = (кроме случаев, когда они используются внутри типа)», чтобы процитировать ошибку, которую вы получите при этом. Конечно, «кроме случаев, когда они используются внутри типа» делает это теоретической возможностью, но не той, которая была бы интересна любому здравомыслящему разработчику.

3 голосов
/ 21 июня 2012

Просто чтобы уточнить. Подход, использующий пустой делегат в качестве начального значения для события, работает даже при использовании с сериализацией:

// 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
1 голос
/ 09 мая 2009

Так же, как примечание, http://blogs.msdn.com/ericlippert/archive/2009/04/29/events-and-races.aspx

Это постоянная ссылка на статью, на которую ссылался Эрик.

0 голосов
/ 09 мая 2009

Brumme - папа для Эрика и Абрамса. Вы должны читать его блог, а не проповедовать ни одному из двух публицистов. Парень серьезно техничен (в отличие от логотипов парикмахеров высокого уровня). Без «Редмонд Флауэрс на 1 ТБ земли» он даст вам правильное объяснение того, почему расы и модели памяти являются проблемой для управляемой (re: shield-the-children) среды, о чем говорил другой плакат выше.

Кстати, все начинается с них, ребята из реализации C ++ CLR:

blogs.msdn.com / cbrumme

0 голосов
/ 08 мая 2009

Что бы это ни стоило, вы должны действительно заглянуть в класс EventsHelper Джувала Лоуи , а не делать все самостоятельно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...