Как правильно отменить регистрацию обработчика событий - PullRequest
64 голосов
/ 15 ноября 2008

В обзоре кода я наткнулся на этот (упрощенный) фрагмент кода, чтобы отменить регистрацию обработчика события:

 Fire -= new MyDelegate(OnFire);

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

Итак, я начал эксперимент:

internal class Program
{
    public delegate void MyDelegate(string msg);
    public static event MyDelegate Fire;

    private static void Main(string[] args)
    {
        Fire += new MyDelegate(OnFire);
        Fire += new MyDelegate(OnFire);
        Fire("Hello 1");
        Fire -= new MyDelegate(OnFire);
        Fire("Hello 2");
        Fire -= new MyDelegate(OnFire);
        Fire("Hello 3");
    }

    private static void OnFire(string msg)
    {
        Console.WriteLine("OnFire: {0}", msg);
    }

}

К моему удивлению, произошло следующее:

  1. Fire("Hello 1"); произвел два сообщения, как и ожидалось.
  2. Fire("Hello 2"); выдано одно сообщение!
    Это убедило меня в том, что незарегистрированный new делегат работает!
  3. Fire("Hello 3"); бросил NullReferenceException.
    Отладка кода показала, что Fire равно null после отмены регистрации события.

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

Чего мне не хватает?

Дополнительный вопрос: из того факта, что Fire - это null, когда нет зарегистрированных событий, я заключаю, что везде, где происходит событие, требуется проверка по null.

Ответы [ 2 ]

82 голосов
/ 15 ноября 2008

Реализация по умолчанию компилятором C # добавления вызовов обработчика событий Delegate.Combine при удалении вызовов обработчика событий Delegate.Remove:

Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));

Реализация Delegate.Remove в Framework не смотрит на сам объект MyDelegate, а на метод, на который ссылается делегат (Program.OnFire). Таким образом, совершенно безопасно создавать новый объект MyDelegate при отписке от существующего обработчика событий. Из-за этого компилятор C # позволяет вам использовать сокращенный синтаксис (который генерирует точно такой же код за кулисами) при добавлении / удалении обработчиков событий: вы можете опустить new MyDelegate part:

Fire += OnFire;
Fire -= OnFire;

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

MyDelegate handler = Fire;
if (handler != null)
    handler("Hello 3");

Он назначается временной локальной переменной для защиты от возможного состояния гонки с отменой подписки на обработчики событий в других потоках. (Подробнее о безопасности потоков при назначении обработчика событий локальной переменной см. в моем блоге ). Другой способ защиты от этой проблемы - создать пустой делегат, который всегда подписан; хотя для этого требуется немного больше памяти, обработчик событий никогда не может быть нулевым (и код может быть проще):

public static event MyDelegate Fire = delegate { };
15 голосов
/ 15 ноября 2008

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

public event MyDelegate Fire = delegate {};

Однако это всего лишь взлом, чтобы избежать исключений NullReferenceExceptions.

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

public event MyDelegate Fire;
public void FireEvent(string msg)
{
    MyDelegate temp = Fire;
    if (temp != null)
        temp(msg);
}

К сожалению, JIT-компилятор может оптимизировать код, исключить временную переменную и использовать исходный делегат. (согласно Juval Lowy - Программирование компонентов .NET)

Таким образом, чтобы избежать этой проблемы, вы можете использовать метод, который принимает делегат в качестве параметра:

[MethodImpl(MethodImplOptions.NoInlining)]
public void FireEvent(MyDelegate fire, string msg)
{
    if (fire != null)
        fire(msg);
}

Обратите внимание, что без атрибута MethodImpl (NoInlining) JIT-компилятор может встроить метод, делая его бесполезным. Поскольку делегаты являются неизменяемыми, эта реализация является поточно-ориентированной. Вы можете использовать этот метод как:

FireEvent(Fire,"Hello 3");
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...