The Problems with Events
There’s no question that the for
loop is a good idea, but events are much more complex. They’re enshrined into C# by the event
keyword, but not everything about them is good. Today’s article shows some considerations you should take into account when deciding whether or not to use an event. Bonus: it includes some little extension methods to make using events and delegates easier!
You can think about events as being a list of functions to call. Subscribing to an event is like adding a function to the list. Unsubscribing is like removing a function from the list. Dispatching the event is like looping over the list calling all the functions. There are some implementation details to these parts, but that’s essentially what’s going on.
When you dispatch an event, you’re admitting to an astonishing level of ignorance. You’re effectively saying that you don’t know which function should be called next. You’re saying that you don’t know how many functions should be called at that time or if any functions should be called at all. And you’re saying you don’t know which order those functions should be called in. An event dispatch tells the compiler “I don’t know what should happen next”.
Making these statements necessarily comes with a lot of downsides. You’ll be creating garbage for the GC. There are dangers such as the event being null
, throwing an exception, or modifying state during the dispatch. And event are just plain slow.
Those are all technical downsides that involve performance and correctness of your code. That’s surely important, but it’s also important to keep the readability of your code in mind. Readability is extremely important while debugging or modifying the code. At this time you’ll go through the familiar “act like a computer” routine. It goes something like this: (quotes are the internal monologue of the programmer reading the code)
int Foo(int a, int b) { // "x is the sum of a and b" var x = a + b; // "c starts at zero" var c = 0; // "keep looping until x isn't positive" while (x > 0) { // "add x to c" // "c must be some kind of accumulator" c += x; // "x gets cut in half each iteration of the loop" // "so c is the sum of each half of x" x /= 2; } // "let's go to Bar now" c = Bar(c); // "c is the result" return c; } // "ah, here's Bar" int Bar(int x) { // "this function just raises to the fourth power" return Math.Pow(x, 4); // "let's go back to Foo now" }
After a little while this kind of reading becomes second nature and we can easily trace through the code to figure out what it’s doing. Then events come along and make the code a lot less readable. Consider this code:
class Person { public string First { get; set; } public string Last { get; set; } public int Age { get; set; } public event Action<int> SomeEvent; public string Foo(int a, int b) { // "x is the sum of a and b" var x = a + b; // "is the event null?" // "how many listeners are there?" // "what order will they be called in?" // "will the listeners change the Person's state?" // "will the listeners throw an exception?" // "what do these listeners do?" // "where do I go to find out?" if (SomeEvent != null) SomeEvent(x); // "did the listeners change any of this state?" // "did the listeners throw an exception and this doesn't execute?" return First + " " + Last + " is " + Age + " years old"; } }
When you’re reading through the code you no longer know what happens when you get to the event dispatch. You need to stop and start to analyze the state of the event at the time it’s dispatched. Remember that the event is basically a list of functions, so it could call any function in the code that has a compatible signature: void Foo(int x)
.
How do you figure out which functions are going to get called? Typically you start by searching the whole codebase for any code that references Person.SomeEvent
. Most IDEs can do this for you, so that’s not so hard.
What is hard is figuring out which functions are subscribed to the event at the time it is dispatched and what order they were subscribed. Now you need to go through each place where the event is subscribed and unsubscribed and try to determine if that code executes before or after the event is dispatched.
Sometimes there’s only one function subscribed to the event and it never gets unsubscribed. In that case it’s pretty easy to find out what happens when the event is dispatched. If there are more functions or the functions get unsubscribed, it gets rapidly more complex.
While you’re going through this process just to determine which functions are being called you’re not thinking about what the functions are doing and how they relate to the what the function that dispatched the event is doing. You’ve increased the reader’s cognitive load and they have to remember a lot more to understand how the code works.
These are serious and unavoidable consequences of using events. You should factor them in to your decision making every time you decide to use an event. It’s good practice to ask yourself—or a teammate—if the upsides outweigh the downsides. I suspect that there are quite a few times where you might decide to not use an event.
So what would you use if you didn’t use an event? You should ask yourself what features of events you were trying to gain when you were tempted to use them. Most of the time this boils down to two main features.
First, events allow one class to call another class without knowing its type. This decouples the two classes because the class A
with the event doesn’t require the class B
with the event listener.
Second, if two classes require references to each other it’s hard to instantiate them because instantiating A
requires B
and instantiating B
requires A
. Events allow us to hook them up after instantiation, but it’s important to remember that the code doesn’t work until after B
adds the event listener to A
.
If these are the reasons why you were tempted to use an event, consider using an interface or a delegate instead. Here’s the “before” code:
class HasEvent { public event Action Event; void Foo() { if (Event != null) Event(); } } class HasListener { public HasListener(HasEvent h) { h.Event += HandleEvent; } private void HandleEvent() { } } var e = new HasEvent(); var l = new HasListener(e);
Notice that HasEvent
doesn’t know anything about HasListener
. It’s decoupled from HasListener
because it doesn’t require one. It doesn’t care if anyone subscribes to the event, how many subscribers there are, or what order they’re in. It’s super generalized and therefore super complex!
Now let’s reduce some of that complexity by swapping out the event for an interface:
interface IListener { void HandleEvent(); } class HasEvent { public IListener Listener { private get; set; } void Foo() { if (Listener != null) Listener.HandleEvent(); } } class HasListener : IListener { public HasListener(HasEvent h) { h.Listener = HandleEvent; } public void HandleEvent() { } } var e = new HasEvent(); var l = new HasListener(e);
It’s basically the same amount of code, but now some of the complexity has been removed. There can be only one listener, which means you only need to look for the one listener that has been set. There can’t be multiple listeners, which also means there can’t be different orders of listeners. The HasEvent
class still doesn’t depend on the HasListener
class. It just depends on the IListener
interface.
Some of the flexibility of events has also been lost. You can’t handle the event anymore with lambdas and your event handler has to be a public function to implement the interface. That might make delegates more attractive than interfaces, so let’s see how that would look:
class HasEvent { public Action Listener { private get; set; } void Foo() { if (Listener != null) Listener(); } } class HasListener { public HasListener(HasEvent h) { h.Listener = HandleEvent; } private void HandleEvent() { } } var e = new HasEvent(); var l = new HasListener(e);
Changing to a delegate allowed us to get rid of the interface, make the event handler function private, and event use lambdas if we wanted to. On the “con” side, it’s slower and much harder to search the code for the function or lambda that gets called when the “event” is dispatched. In this case it could be any function or lambda with the void Foo()
signature. There are probably tons of functions like that in your codebase, but only a handful of classes implementing IListener
.
Lastly for the alternatives, consider coupling the classes. Hear me out! In a lot of cases you only ever have one class that handles the “event” anyway. You can always switch to an interface, delegate, or event later on if you add a second one. And if you take this approach then the code gets even simpler:
class HasEvent { public HasListener Listener { private get; set; } void Foo() { if (Listener != null) Listener.HandleEvent(); } } class HasListener { public HasListener(HasEvent h) { h.Listener = HandleEvent; } public void HandleEvent() { } } var e = new HasEvent(); var l = new HasListener(e);
With this version of the code we’re back to the very first example where Foo
calls Bar
directly. There’s no question about which function is getting called- we can jump straight there! The code is extremely easy to read and follow with none of the complexity that came with events, lambdas, or interfaces. There’s only one listener function and we know exactly which one it is. It’s not even a virtual function, so the call will be super fast.
The obvious downside is that the two classes are now coupled, the “event” handler has to be public, we can’t handle it with lambdas, and we can only have one handler function.
Consider the downsides of events when choosing how you want one class to call another. Don’t just opt for the most general, most complex option because it happens to have a keyword in C#. If you don’t need all of the flexibility of events—and you often don’t—then consider interfaces, lambdas, or just plain old function calls. There are huge advantages to be had!
Finally, if you’d still like to use events or you’re opting for the delegate approach then I have a little extension function for you. In the above code you continually see this: if (Listener != null) Listener()
. That’s because adding a default, no-op listener to the event is expensive so you need to check if the event is null
before dispatching it. These extension functions do that for you!
using System; /// <summary> /// Extension functions to System.EventHandler and System.Action /// http://JacksonDunstan.com/articles/3621 /// License: MIT /// </summary> public static partial class DelegateExtensions { /// <summary> /// Dispatch the EventHandler delegate if it's not null /// </summary> /// <param name="del">EventHandler delegate to dispatch</param> /// <param name="sender">Sender object to pass</param> /// <param name="eventArgs">Event arguments to pass</param> /// <typeparam name="T">Type of EventArgs to dispatch</typeparam> public static void Dispatch<T>(this EventHandler<T> del, object sender, T eventArgs) where T : EventArgs { if (del != null) { del(sender, eventArgs); } } /// <summary> /// Dispatch the Action delegate if it's not null /// </summary> /// <param name="del">Action delegate to dispatch</param> /// <param name="e">Value argument to pass</param> public static void Dispatch<T>(this Action<T> del, T e) { if (del != null) { del(e); } } /// <summary> /// Dispatch the Action delegate if it's not null /// </summary> /// <param name="del">Action delegate to dispatch</param> /// <param name="e1">First value argument to pass</param> /// <param name="e2">Second value argument to pass</param> public static void Dispatch<T1, T2>(this Action<T1, T2> del, T1 e1, T2 e2) { if (del != null) { del(e1, e2); } } /// <summary> /// Dispatch the Action delegate if it's not null /// </summary> /// <param name="del">Action delegate to dispatch</param> /// <param name="e1">First value argument to pass</param> /// <param name="e2">Second value argument to pass</param> /// <param name="e3">Third value argument to pass</param> public static void Dispatch<T1, T2, T3>(this Action<T1, T2, T3> del, T1 e1, T2 e2, T3 e3) { if (del != null) { del(e1, e2, e3); } } }
Use it like this:
public class SomeClass { public class MyEventArgs : EventArgs { } public EventHandler<MyEventArgs> MyEvent; public SomeClass() { // Problem: this throws a NullReferenceException MyEvent(this, new MyEventArgs()); // The extension function checks for you! This doesn't throw an exception. MyEvent.Dispatch(this, new MyEventArgs()); // Of course everything works as expected after adding a listener MyEvent += (sender, e) => Debug.Log("handler called"); MyEvent.Dispatch(this, new MyEventArgs()); // prints the log } }
The extension functions only work for events that use EventHandler
and Action
(up to three parameters) as the delegate type, but it’s easily modified to support more types of delegates or more parameters to Action
.
Do you use events or prefer something like interfaces instead? Has this article changed your mind? Let’s discuss in the comments!