Как правильно отменить регистрацию обработчика событий
В обзоре кода я наткнулся на этот (упрощенный) фрагмент кода, чтобы отменить регистрацию обработчика событий:
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);
}
}
К моему удивлению, произошло следующее:
-
Fire("Hello 1");
вышло два сообщения, как и ожидалось.
-
Fire("Hello 2");
создано одно сообщение!
Это убедило меня в том, что работают дезарегистрирующие делегаты new
!
-
Fire("Hello 3");
выбрал NullReferenceException
.
Отладка кода показала, что Fire
является null
после отмены регистрации события.
Я знаю, что для обработчиков событий и делегата компилятор генерирует много кода за сценой. Но я до сих пор не понимаю, почему мои рассуждения ошибочны.
Что мне не хватает?
Дополнительный вопрос: из-за того, что Fire
есть null
, когда нет зарегистрированных событий, я делаю вывод, что везде, где происходит событие, требуется проверка против null
.
Ответы
Ответ 1
Реализация по умолчанию компилятора С# для добавления обработчика событий вызывает Delegate.Combine
, удаляя вызовы обработчика событий Delegate.Remove
:
Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));
Реализация Framework Delegate.Remove
не относится к самому объекту MyDelegate
, но к методу, на который делегат ссылается (Program.OnFire
). Таким образом, совершенно безопасно создавать новый объект MyDelegate
при отмене подписки на существующий обработчик событий. Из-за этого компилятор С# позволяет использовать сокращенный синтаксис (который генерирует точно такой же код за кулисами) при добавлении/удалении обработчиков событий: вы можете опустить часть new MyDelegate
:
Fire += OnFire;
Fire -= OnFire;
Когда последний делегат удален из обработчика события, Delegate.Remove
возвращает значение null. Как вы выяснили, необходимо проверить событие против null до его повышения:
MyDelegate handler = Fire;
if (handler != null)
handler("Hello 3");
Он назначается временной локальной переменной для защиты от возможного состояния гонки с отменой подписки на обработчики событий на других потоках. (См. мой пост в блоге для получения подробной информации о безопасности потоков при назначении обработчика события локальной переменной.) Другой способ защиты от этой проблемы - создать пустой делегат, который всегда подписывается; в то время как это использует немного больше памяти, обработчик события никогда не может быть нулевым (и код может быть проще):
public static event MyDelegate Fire = delegate { };
Ответ 2
Вы должны всегда проверять, нет ли у делегата никаких целей (его значение равно null), прежде чем запускать его.
Как было сказано ранее, один из способов сделать это - подписаться с анонимным методом do-nothing, который не будет удален.
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");