Sponsored By

C# Events: Null checking or initializing with do-nothing delegate

There are a few ways to invoke C# events. By default, with no subscribers, an event is null. Therefore, to invoke it, we need to first check if it is null. Another option is to initialize the event with a do-nothing delegate. Which way is better?

Alexey Khazov, Blogger

June 9, 2020

4 Min Read

Invoking C# events

C# events are very useful for loosely decoupling classes and objects. They also allow to act only when something specific happens in the program instead of continuously polling for state.

One common way to declare events in C# is as follows:


public event Action SomethingHappened;

By default, when an event has no subscribers, it is equal to null. This means that to invoke the event, developers need to first check if it is null, and only then invoke it:


if(SomethingHappened != null)
{
    SomethingHappened.Invoke();
}

With the release of C# 6, developers got the amazing new null-propagating operator - '?.'. The way the operator works is that it checks whether the statement on the left of the question mark is null. If it is, then the operation short-circuits and does not continue to call whatever is after the dot. If it isn't, the operation continues past the dot as usual.

Using this operator, the above invocation gets simplified to:


SomethingHappened?.Invoke();

Now, in case SomethingHappened is null, Invoke() will not be called. However, if SomethingHappened is not null, Invoke() will be called as usual.

Another popular way to invoke events is to avoid the null-check completely by initializing the event with a do-nothing delegate:


public event Action SomethingHappened = delegate{};

With this approach, the event always has at least one subscriber, and is, therefore, never null. This means that the event can now be invoked with a simple call to Invoke():


SomethingHappened.Invoke();

Which of the two ways is better? 

While the second method seems more convenient as it avoids unnecessary null-checks, it adds an additional delegate that gets calls every time the event is invoked, even though that delegate does absolutely nothing. This means that it decreases performance. Let's check it out:

Running a quick benchmark (code available below), yields the following results:


Running benchmark 'Event with empty delegate no other listeners' for 100000 iterations... Elapsed time 2.647 ns.

Running benchmark 'Event null no other listeners' for 100000 iterations... Elapsed time 1.44 ns.

Running benchmark 'Event with empty delegate w/ listener' for 100000 iterations... Elapsed time 7.623 ns.

Running benchmark 'Event null w/ listener' for 100000 iterations... Elapsed time 3.324 ns.

Running benchmark 'Event with empty delegate w/ 10000 listeners' for 100000 iterations... Elapsed time 23938.374 ns.

Running benchmark 'Event null w/ 10000 listeners' for 100000 iterations... Elapsed time 23870.421 ns.

When there are no listeners at all, the null event is about twice as fast. The same is true with 1 listener.

The performance benefits are lost as more and more listeners are added, which makes sense - the difference between the two will always be just 1 extra listener.

Above that, the runtimes are all in nanoseconds - that's 10^-9 seconds, or 0.000000001 seconds. Performance differences are very negligible, but they still exist. 

Furthermore, always declaring an event as containing a subscriber removes possible functionality where a program needs to know when no one is listening. It is probably a rare case, but it is still a loss of functionality.

Personally, I also think that it is easier to use ?. than to remember to add = delegate {} for every event declaration. Therefore, while the performance difference is negligible, it is still slightly faster and more convenient to use the new C# 6 ?. operator to invoke events instead of pre-initializing them with a do-nothing delegate.

Benchmark code


using System;
using System.Diagnostics;
 
namespace EventsBenchmark
{
    internal class Program
    {
        private static event Action eventWithEmptyDelegate = delegate {};
        private static event Action eventNull = null;
       
        public static void Main(string[] args)
        {
            Benchmark("Event with empty delegate no other listeners", 100000, () => eventWithEmptyDelegate.Invoke());
            Benchmark("Event null no other listeners", 100000, () => eventNull?.Invoke());
 
            eventWithEmptyDelegate += DoStuff;
            eventNull += DoStuff;
           
            Benchmark("Event with empty delegate w/ listener", 100000, () => eventWithEmptyDelegate.Invoke());
            Benchmark("Event null w/ listener", 100000, () => eventNull?.Invoke());
 
            eventWithEmptyDelegate -= DoStuff;
            eventNull -= DoStuff;
           
            const int listenerCount = 10000;
            for (var i = 0; i < listenerCount; ++i)
            {
                eventWithEmptyDelegate += DoStuff;
                eventNull += DoStuff;
            }
           
            Benchmark($"Event with empty delegate w/ {listenerCount} listeners", 100000, () => eventWithEmptyDelegate.Invoke());
            Benchmark($"Event null w/ {listenerCount} listeners", 100000, () => eventNull?.Invoke());
        }
 
        private static void DoStuff()
        { }
 
        private static void Benchmark(string name, int iterations, Action action)
        {
            try
            {
                Console.Write($"Running benchmark '{name}' for {iterations} iterations... ");
 
                // Perform garbage collection.
                GC.Collect();
                GC.WaitForPendingFinalizers();
 
                // Force JIT compilation of the method.
                action.Invoke();
 
                // Run the benchmark.
                var watch = Stopwatch.StartNew();
                for (var i = 0; i < iterations; ++i)
                {
                    action.Invoke();
                }
                watch.Stop();
           
                // Output results.
                Console.WriteLine($"Elapsed time {watch.Elapsed.TotalMilliseconds * 1000000 / iterations} ns.");
            }
            catch (OutOfMemoryException)
            {
                Console.WriteLine("Out of memory!");
            }
        }
    }
}

 

Read more about:

Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like