Подтвердить что ты не робот

Почему С# требует, чтобы вы записывали нулевую проверку каждый раз, когда вы запускаете событие?

Мне кажется странным - VB.NET выполняет неявное обращение с помощью ключевого слова RaiseEvent. Похоже, что значительно увеличивает количество шаблонов вокруг событий, и я не вижу, какую пользу он предоставляет.

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

4b9b3361

Ответ 1

Это, конечно, досада.

Когда вы пишете код, который обращается к полюподобному событию внутри класса, вы фактически обращаетесь к самому полю (по модулю несколько изменений на С# 4, не оставляйте его на данный момент).

Таким образом, параметры:

  • Особые случаи, связанные с полевыми вызовами, чтобы они фактически не ссылались на поле напрямую, а вместо этого добавляли обертку
  • Обработать все вызовы делегатов по-другому, например:

    Action<string> x = null;
    x();
    

    не будет генерировать исключение.

Конечно, для непустых делегатов (и событий) оба варианта поднимают проблему:

Func<int> x = null;
int y = x();

Должно ли это молча вернуться 0? (Значение по умолчанию для int.) Или это фактически маскирует ошибку (более вероятно). Было бы несколько непоследовательно заставить его молча игнорировать тот факт, что вы пытаетесь вызвать нулевой делегат. В этом случае было бы еще более странно, что не использует синтаксический сахар С#:

Func<int> x = null;
int y = x.Invoke();

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

Ответ 2

мы обычно обходим это, объявляя наши события следующим образом:

public event EventHandler<FooEventArgs> Foo = delegate { };

это имеет два преимущества. Во-первых, мы не проверяем значение null. Во-вторых, мы избегаем критической проблемы раздела, которая вездесущая при типичном запуске события:

// old, busted code that happens to work most of the time since
// a lot of code never unsubscribes from events
protected virtual void OnFoo(FooEventArgs e)
{
    // two threads, first checks for null and succeeds and enters
    if (Foo != null) {
        // second thread removes event handler now, leaving Foo = null
        // first thread resumes and crashes.
        Foo(this, e);
    }
}

// proper thread-safe code
protected virtual void OnFoo(FooEventArgs e)
{
     EventHandler<FooEventArgs> handler = Foo;
     if (handler != null)
         handler(this, e);
}

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

protected virtual void OnFoo(FooEventArgs e)
{
    Foo(this, e); // always good
}

С извинениями перед Пэт Моритой в "Каратэ Кид" "Лучший способ избежать нулевого значения не имеет этого".

Что касается того, почему С# не обманывает вас так же, как VB. Хотя ключевое слово event скрывает большинство деталей реализации многоадресных делегатов, это дает вам более тонкий контроль, чем VB.

Ответ 3

Вам нужно подумать над тем, какой код понадобится, если настройка сантехники для поднятия события в первую очередь будет дорогостоящей (например, SystemEvents) или при подготовке аргументов события будет дорогостоящей (например, событие Paint).

Стиль обработки событий Visual Basic не позволяет отложить затраты на поддержку такого события. Вы не можете переопределить добавление/удаление аксессуаров, чтобы задержать установку дорогостоящей сантехники. И вы не можете обнаружить, что не могут быть какие-либо обработчики событий, подписанные, так что сжигание циклов для подготовки аргументов события является пустой тратой времени.

Не проблема в С#. Классический компромисс между удобством и контролем.

Ответ 4

Удлинительные методы обеспечивают очень хороший способ, чтобы обойти это. Рассмотрим следующий код:

static public class Extensions
{
    public static void Raise(this EventHandler handler, object sender)
    {
        Raise(handler, sender, EventArgs.Empty);
    }

    public static void Raise(this EventHandler handler, object sender, EventArgs args)
    {
        if (handler != null) handler(sender, args);
    }

    public static void Raise<T>(this EventHandler<T> handler, object sender, T args)
        where T : EventArgs
    {
        if (handler != null) handler(sender, args);
    }
}

Теперь вы можете просто сделать это:

class Test
{
    public event EventHandler SomeEvent;

    public void DoSomething()
    {
        SomeEvent.Raise(this);
    }
}

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

Ответ 5

Не знаю, почему это сделано, но существует вариант Нулевой шаблон объекта специально для делегатов:

private event EventHandler Foo = (sender, args) => {};

Таким образом вы можете свободно ссылаться на Foo, не проверяя при этом null.

Ответ 6

Потому что RaiseEvent несет некоторые накладные расходы.

Всегда есть компромисс между контролем и простотой использования.

  • VB.Net: простота использования,
  • С#: больше контроля как VB
  • С++: еще больше контроля, меньше указаний, легче стрелять в ногу.

Ответ 7

Изменить: Как указывает OP, этот ответ не затрагивает суть вопроса. Тем не менее, некоторые из них могут оказаться полезными, поскольку они дают ответ на заголовок вопроса (когда он сделан сам):

Почему С# требует, чтобы вы записывали нулевую проверку каждый раз, когда вы запускаете событие?

В нем также содержится контекст для намерения органа вопроса, который некоторые могут найти полезным. Итак, по этим причинам и этот совет на Meta, я дам этому ответу.


Исходный текст:

В статье MSDN Как опубликовать события, соответствующие требованиям .NET Framework (Руководство по программированию на С#) (Visual Studio 2013), В следующем примере Microsoft включает следующий комментарий:

// Make a temporary copy of the event to avoid possibility of 
// a race condition if the last subscriber unsubscribes 
// immediately after the null check and before the event is raised.

Вот более крупный отрывок из примера кода Microsoft, который дает контекст этому комментарию.

// Wrap event invocations inside a protected virtual method 
// to allow derived classes to override the event invocation behavior 
protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
{
    // Make a temporary copy of the event to avoid possibility of 
    // a race condition if the last subscriber unsubscribes 
    // immediately after the null check and before the event is raised.
    EventHandler<CustomEventArgs> handler = RaiseCustomEvent;

    // Event will be null if there are no subscribers 
    if (handler != null)
    {
        // Format the string to send inside the CustomEventArgs parameter
        e.Message += String.Format(" at {0}", DateTime.Now.ToString());

        // Use the () operator to raise the event.
        handler(this, e);
    }
}

Ответ 8

Причина действительно сводится к тому, что С# дает вам больше контроля. В С# вам не нужно делать проверку null, если вы так решите. В следующем коде MyEvent никогда не может быть null, так зачем беспокоиться о выполнении проверки?

public class EventTest
{
  private event EventHandler MyEvent = delegate { Console.WriteLine("Hello World"); }

  public void Test()
  {
    MyEvent(this, new EventArgs());
  }
}

Ответ 9

Обратите внимание, что с С# 6 язык теперь предоставляет сжатый синтаксис для выполнения этой нулевой проверки. Например:.

public event EventHandler SomeEvent;

private void M()
{
    // raise the event:
    SomeEvent?.Invoke(this, EventArgs.Empty);
}

См. Null Conditional Operator