In a code review, I stumbled over this (simplified) code fragment to unregister an event handler:
Fire -= new MyDelegate(OnFire);
I thought that this does not unregister the event handler because it creates a new delegate which had never been registered before. But searching MSDN I found several code samples which use this idiom.
So I started an experiment:
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); } }
To my surprise, the following happened:
Fire('Hello 1');produced two messages, as expected.Fire('Hello 2');produced one message!
This convinced me that unregisteringnewdelegates works!Fire('Hello 3');threw aNullReferenceException.
Debugging the code showed thatFireisnullafter unregistering the event.
I know that for event handlers and delegate, the compiler generates a lot of code behind the scene. But I still don’t understand why my reasoning is wrong.
What am I missing?
Additional question: from the fact that Fire is null when there are no events registered, I conclude that everywhere an event is fired, a check against null is required.
The C# compiler’s default implementation of adding an event handler calls
Delegate.Combine, while removing an event handler callsDelegate.Remove:The Framework’s implementation of
Delegate.Removedoesn’t look at theMyDelegateobject itself, but at the method the delegate refers to (Program.OnFire). Thus, it’s perfectly safe to create a newMyDelegateobject when unsubscribing an existing event handler. Because of this, the C# compiler allows you to use a shorthand syntax (that generates exactly the same code behind the scenes) when adding/removing event handlers: you can omit thenew MyDelegatepart:When the last delegate is removed from the event handler,
Delegate.Removereturns null. As you have found out, it’s essential to check the event against null before raising it:It’s assigned to a temporary local variable to defend against a possible race condition with unsubscribing event handlers on other threads. (See my blog post for details on the thread safety of assigning the event handler to a local variable.) Another way to defend against this problem is to create an empty delegate that is always subscribed; while this uses a little more memory, the event handler can never be null (and the code can be simpler):