Правильный способ создания событий в .NET framework

В настоящее время "Избегайте проверки для обработчиков событий" в верхней части ответов на сообщение под заголовком Скрытые возможности С# и содержит очень вводящую в заблуждение информацию.

Хотя я понимаю, что Qaru - это "демократия", и ответ поднялся на вершину благодаря публичному голосованию, я чувствую, что многие люди, которые голосовали за ответ, либо не имели полного понимания С#/.NET или не нашли времени, чтобы полностью понять последствия практики, описанной в сообщении.

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

public event EventHandler SomeEvent = delegate {};
// Later..
void DoSomething()
{
   // Invoke SomeEvent without having to check for null reference
    SomeEvent(this, EventArgs.Empty);  
}

На первый взгляд это может показаться умным ярлыком, но это может быть причиной серьезных головных болей в большом приложении, особенно если задействован concurrency.

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

Что-то вроде этого типично:

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 - Дизайн событий. Особенно обсуждались темы Эрика Гуннерсона и Джо Даффи.

Как было предложено Эриком, в одном из приведенных ниже ответов я должен указать, что может быть разработано лучшее решение для синхронизации, которое позаботится об этой проблеме. Моя цель с этой должности состояла в том, чтобы повысить уровень осведомленности и , чтобы не дать решение с единственным и единственным истинным решением проблемы. Как предложил Эрик Липперт и Эрик Гуннерсон в вышеупомянутой книге, конкретное решение проблемы зависит от программиста, но важно то, что проблема не игнорируется.

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

Ответ 1

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

События С# и безопасность потоков

Ваше резюме ничего не делает, чтобы убедить меня в другом!

Во-первых, клиенты класса не могут присваивать значение 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, но затем формулирует другой, но похожий вопрос. Думаю, он сделал это для законной риторической цели - для того, чтобы установить сцену для обсуждения состояния вторичной расы, которая влияет на обработчиков события. Но, к сожалению, если вы переходите по ссылке на мой вопрос, а затем читаете его сообщение в блоге немного небрежно, у вас может сложиться впечатление, что он отклоняет метод "пустой делегат".

Фактически, он говорит: "Есть и другие способы решения этой проблемы, например, инициализация обработчика, чтобы иметь пустое действие, которое никогда не удаляется", что является методом "пустой делегат".

Он закрывает "выполнение нулевой проверки", потому что это "стандартный шаблон"; мой вопрос: почему это стандартный шаблон? Джон Скит предположил, что, учитывая, что совет предшествует добавлению анонимных функций на язык, это, вероятно, просто похмелье от С# версии 1, и я думаю, что это почти наверняка верно, поэтому я принял его ответ.

Ответ 2

"Просто потому, что вы инициализировали событие пустым делегатом, это не означает, что пользователь вашего класса не будет устанавливать его в нуль в какой-то момент и сломает ваш код".

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

Ответ 3

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

// 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

Ответ 4

Для чего это стоит, вы должны действительно изучить Juval Lowy EventsHelper class вместо того, чтобы делать что-то самостоятельно.

Ответ 6

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

Btw, все начинается с них, ребята из С++ CLR:

blogs.msdn.com/cbrumme