GOTCHA #7 Uninitialized event handlers aren't treated gracefullyDelegates are very effective for implementing callbacks in .NET. A delegate encapsulates a pointer to a method, and an instance of an object on which that method needs to be executed. A delegate can also encapsulate a pointer to a static/Shared method. The syntax provided to use a delegate is intuitive. You do not have to deal with messy pointers to functions as in C++. Delegates are used to specify the handlers that will be called when an event occurs. If you want to register multiple methods of a class as event handlers, you can do so very easily without having to resort to something as complicated as anonymous inner classes, as you do in Java. In order to call the handler that a delegate represents, you can either use the DynamicInvoke() method, or you can just call the delegate as if it were itself a method: MyDelegate.DynamicInvoke(...) Or: MyDelegate(...) Their ease of use sometimes obscures the fact that delegates are just classes, created when the compiler sees the delegate keyword. When you use a delegate, you are using an object through a special syntax. Of course, you know not to invoke methods on an object reference that you haven't initialized. However, it may not be readily apparent when a delegate is uninitialized. When raising an event, you should consider the possibility that no handlers have been added or registered. Consider the code in Example 1-13. Example 1-13. Accessing an uninitialized delegateC# (Delegate) // AComponent.cs using System; namespace UnInitializedDelegate { public delegate void DummyDelegate(); public class AComponent { public event DummyDelegate myEvent; protected virtual void OnMyEvent() { myEvent(); } public void Fire() { Console.WriteLine("Raising event"); OnMyEvent(); // Raising the event Console.WriteLine("Done raising event"); } } } //Test.cs using System; namespace UnInitializedDelegate { public class Test { private void callback1() { Console.WriteLine("callback1 called"); } private void callback2() { Console.WriteLine("callback2 called"); } private void Work() { AComponent obj = new AComponent(); Console.WriteLine("Registering 2 callbacks"); obj.myEvent += new DummyDelegate(callback1); obj.myEvent += new DummyDelegate(callback2); obj.Fire(); Console.WriteLine("Removing 1 callback"); obj.myEvent -= new DummyDelegate(callback2); obj.Fire(); Console.WriteLine("Removing the other callback"); obj.myEvent -= new DummyDelegate(callback1); obj.Fire(); } [STAThread] static void Main(string[] args) { Test testObj = new Test(); testObj.Work(); } } } VB.NET (Delegate) 'AComponent.vb Public Delegate Sub DummyDelegate() Public Class AComponent Public Event myEvent As DummyDelegate Protected Overridable Sub OnMyEvent() RaiseEvent myEvent() End Sub Public Sub Fire() Console.WriteLine("Raising event") OnMyEvent() ' Raising the event Console.WriteLine("Done raising event") End Sub End Class 'Test.vb Public Class Test Private Sub callback1() Console.WriteLine("callback1 called") End Sub Private Sub callback2() Console.WriteLine("callback2 called") End Sub Private Sub Work() Dim obj As New AComponent Console.WriteLine("Registering 2 callbacks") AddHandler obj.myEvent, New DummyDelegate(AddressOf callback1) AddHandler obj.myEvent, New DummyDelegate(AddressOf callback2) obj.Fire() Console.WriteLine("Removing 1 callback") RemoveHandler obj.myEvent, New DummyDelegate(AddressOf callback2) obj.Fire() Console.WriteLine("Removing the other callback") RemoveHandler obj.myEvent, New DummyDelegate(AddressOf callback1) obj.Fire() End Sub Shared Sub Main(ByVal args As String()) Dim testObj As New Test testObj.Work() End Sub End Class When executed, the C# version of the program produces the result shown in Figure 1-10. As Figure 1-10 shows, a NullReferenceException is thrown when the third call to the Fire() method tries to raise the event. The reason for this is that no event handler delegates are registered at that moment. Figure 1-10. Output from the C# version of Example 1-13The VB.NET version of the program, however, does not throw an exception. It works just fine.[*] Why? In the MSIL generated for RaiseEvent() (shown in Example 1-14), a check for the reference being Nothing is made.
Example 1-14. MSIL translation of a RaiseEvent() statement IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld class UnInitializedDelegate.DummyDelegate UnInitializedDelegate.AComponent::myEventEvent IL_0007: brfalse.s IL_0015 IL_0009: ldarg.0 IL_000a: ldfld class UnInitializedDelegate.DummyDelegate UnInitializedDelegate.AComponent::myEventEvent IL_000f: callvirt instance void UnInitializedDelegate.DummyDelegate::Invoke() IL_0014: nop IL_0015: nop
The correct way to implement this code in C# is to program defensively by checking for a null reference before raising the event, as shown in Example 1-15. Example 1-15. Checking for an uninitialized delegateC# (Delegate) protected virtual void OnMyEvent() { if(myEvent != null) { myEvent(); } } Checking to see if the delegate is not null prevents the NullReferenceException. The delegate will be null if no one has asked to be notified when the event triggers. Note that there is still a problem. It is possible that the last registered event handler has been removed between the line where you check if myEvent is null and the line where you raise the event, and the code may still fail. You need to consider this possibility and raise the event in a thread-safe way. See Gotcha #64, "Raising events lacks thread-safety" for details on this. IN A NUTSHELLUse caution when raising an event. If no event handler has been registered, an exception is thrown in C# when you raise an event. Check to make sure that the delegate is not null before raising the event. In both C# and VB.NET, you need to worry about thread-safety when raising events. SEE ALSOGotcha #64, "Raising events lacks thread-safety." |