Behind the scenes of events

Events are a classic implementation of the observer pattern. Support for events syntax exists in many languages, such as C#. In this post I’ll explain the internals of events.

Delegates’ background

The most abstract way to describe a delegate is a “pointer to a method”. A very relevant feature of delegates is that they can “point” at multiple methods. In order to do so we use the =+ operator and combine to other delegates, for example:

[Test]
public void Invoke_TwoDelegatesCombined_BothCalled()
{
bool wasACalled = false;
bool wasBCalled = false;

Action delA = () => wasACalled = true;
Action delB = () => wasBCalled = true;

Action combine = null;
combine += delA;
combine += delB;

combine.Invoke();

Assert.That(wasACalled);
Assert.That(wasBCalled);
}

But, what actually happens here? Let’s take a look at this code:

combine += delA;

This code compiles to the following IL:

IL_0031: ldloc.2
IL_0032: ldloc.0
IL_0033: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
IL_0038: castclass [mscorlib]System.Action
IL_003d: stloc.2

Which is equivalent to:

combine = Delegate.Combine(combine, delA);

The result of the compiled code is direct call to Delegate.Combine, which makes any future call to the combined result be forwarded to both delegates.

Default event

If we use default event implementation, the compiler generates two methods and a backing field. The backing field is a delegate storing the subscribers; the methods are add and remove the subscribers from the delegate. This implementation allows us to add and remove subscribers for whom the event is visible and raise the event from the type itself. For example:

public class Publisher
{
public event EventHandler MyEvent;

public void Publish()
{
MyEvent(
this, EventArgs.Empty);
}
}

The event compiles into a field, which is a delegate of the event type:

.field private class [mscorlib]System.EventHandler MyEvent

And into two methods for adding and removing subscribers:

.event [mscorlib]System.EventHandler MyEvent
{
.addon instance void Events.Publisher::add_MyEvent(class [mscorlib]System.EventHandler)
.removeon instance void Events.Publisher::remove_MyEvent(class [mscorlib]System.EventHandler)
}

With the signatures:

.method public hidebysig specialname 
instance void add_MyEvent (
class [mscorlib]System.EventHandler 'value'
) cil managed

.method public hidebysig specialname
instance void remove_MyEvent (
class [mscorlib]System.EventHandler 'value'
) cil managed

The bodies of the events, not surprisingly, manipulate the backing field; we can ignore the bodies for now.
So up to here we see what the declaration of event compiles into – an event declaration, a backing field which is a delegate of the event type and two methods for adding and removing subscribers. All this magic from a single C# line of code.
The other side of the event is what happens as we raise it. The event can be raised only from within the type that declares it. For example:

MyEvent(this, EventArgs.Empty);

Compiles into:

IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld class [mscorlib]System.EventHandler Events.Publisher::MyEvent
IL_0007: ldarg.0
IL_0008: ldsfld class [mscorlib]System.EventArgs [mscorlib]System.EventArgs::Empty
IL_000d: callvirt instance void [mscorlib]System.EventHandler::Invoke(object, class [mscorlib]System.EventArgs)

All this code does is accessing the delegate backing field and invoking it.

Custom event

In fact, the event is not custom but the add/remove methods are. Custom add/remove for events is a feature in C# which I think is not very commonly used (in contrast to properties). It allows the developer to provide an alternative implementation to the event subscription.

public event EventHandler MyCustomEvent
{
add {}
remove { }
}

The compiled class in this case does not contain a backing field. It contains the declaration of the event and the two methods with the custom provided body.
A difference which derives from the compiled code difference is that there’s no way to raise the event directly. This makes sense since the custom code can do many things (or nothing) with the subscribers and not store them in a common place for later invocation. If we try to raise MyCystomEvent in the same way we tried to raise MyEvent we’ll get a compilation error.

4 responses to “Behind the scenes of events

  1. The most common example (at least that I had a chance to see in real life) is registering “logical” events on single delegate. Meaning, two events add and remove their subscribers from the same delegate. For example – 'NameChanged', 'FirstNameChanged' & 'LastNameChanged', if a subscriber registers to NameChanged, we actually want to register it to the other two events.

    Another one I can think about is optimization – assume we have a class whose job is to track file system changes and raise events according to it. If there are no subscribers registered, there's no need to actually track the file system. In this case, on add and remove we can check if there are actual subscribers and start/stop tracking the file system accordingly.

    Like

  2. I used custom event adders when I wanted to make sure that late coming subscribers get the event even if it happened in the past.

    For instance, an input file is needed to start running a program, but may arrive either before or after the program starts running. We want a single API to notify the consumer that the file arrived, instead of splitting it between checking at startup and registering for an event. For instance:

    public event FileArrived
    {
    add
    {
    if(alreadyArrived)
    value(this, …);
    }

    }

    Like

Leave a comment