C# Delegates And Events in Depth

After a couple of years of programming in .NET I finally took time to investigate in detail how exactly C# delegates and events work under the hood. Topics covered in this article include:

Delegate Class

Every delegate object in your code is implicitly derived from class Delegate. This class is special in a sense that only system and compilers and create types derived from it. C# and VB.NET programmers cannot derive explicitly from Delegate. I am not sure whether this limitation holds for programs written directly in IL (will verify it some day).

Delegate class has two main properties - Target and Method.Target references an object instance, and Method describes a method implemented by that instance. To invoke a delegate means to take Target object and call its method described in Method.

For example:

delegate void MyDelegate();
        
class MyClass
{
    public void SomeMethod() {...}
}

...
MyClass myClass = new MyClass();
MyDelegate d = new MyDelegate( myClass.SomeMethod );

// now d.Target is myClass, and d.Method describes MyClass.SomeMethod

        

Note that Delegate represents a unicast delegate, which referes to a single method.

Delegates and Static Methods

What is the Target of a delegate which points to a static method?

For the outside world, Target property of such a delegate returns null. But this is not the whole story. Internally delegate stores its target in a private field named _target. Normally, public property Target just returns a value of _target. Not so for static delegates. For static delegates, _target contains a reference to the delegate itself. "Get" accessor of Target makes a quick check, and if _target references a delegate, returns null.

I am not sure what are the reasons for this balancing act.

Equality of Delegates

Non-static delegates are equal if their targets are the same (i.e. are ReferebceEqual()), and their Methods point to the same method (i.e. are Equal()). This means that delegates can be considered equal even if they belong to different delegate types.

For instance, example below prints "Delegate equality: delegates are equal".

public class DelegateEquality
{
    delegate void DelegateTypeA();
    delegate void DelegateTypeB();

    private void TestMethod() {}

    public void DoTest()
    {
        DelegateTypeA a = new DelegateTypeA(this.TestMethod);
        DelegateTypeB b = new DelegateTypeB(this.TestMethod);
        bool areEqual = Object.Equals(a,b);
        if (areEqual)
        {
            Console.WriteLine("Delegate equality: delegates are equal");
        }
        else
        {
            Console.WriteLine("Delegate equality: delegates are not equal");
        }
    }
}

Static delegates are equal if their Methods point to the same method.

MulticastDelegate Class

Class MulticastDelegate derives from Delegate and represents a delegate that can invoke more than one method at once. All delegates that you create in C# are multicast delegates.

Internally, MulticastDelegate is implemented as a linked list of delegates. Delegates are stored in the list in the reversed order of execution. I.e. if we execute the following code:

Class1 object1 = new Class1();
Class2 object2 = new Class2();     
MyDelegate d = new MyDelegate(object1.Method1);
d += new MyDelegate(object2.Method2);

delegate d is represented in memory as follows:

This representation is convenient, because additions and removals are usually done in the end of the list. Invoking the delegate is implemented via recursion: invocation code first invokes Previous delegate (which recursively invokes the whole chain), and then calls Target.Method.

Note that due to this representation Target and Method properties of a multicast delegate always return Target and Method of the delegate most recently added to the list.

Adding and Removing Delegates

All delegate types declared in C# have implicit operators += and -=. Adding a delegate to a multicast is straightforward: a+=b appends b to the invocation list of a. Removing delegates is based on equality. a-=b removes last occurence (in execution order) of a delegate equal to b from the invocation list of a. If invocation list of a becomes empty, a becomes null.

Removal by equality allows us to get away with code like this:

MyDelegate a = null;
a += new MyDelegate(object1.Method1);
a -= new MyDelegate(object1.Method1);

Delegates passed to += and -= are not the same, but they are equal. Since removal is based on logical equality, and not reference equality, it works fine in this case and a becomes null as we expect it to.

Null Delegates

There is no such thing as delegate without an invocation list. Whenever invocation list becomes empty, delegate becomes null. Calling null delegate is a run-time error. Therefore, whenever someone calls a delegate, he/she must check it for null, or else.

MyDelegate a;
...
if (a != null) { a( parameters ); }

I am not sure why Microsoft decided not to make this check automatic. After all, calling a delegate without the check rarely makes sense. A situation when we can absolutely guarantee that the delegate is not null, and when we do really worry about extra couple of cycles spent on the check, seems to occur pretty rarely. If really necessary, for the situation like this a special way to do "nocheck" calls could be provided, but checked call should be the default. Unfortunately, it isn't.

Events

When I started programming in C#, it took me certain effort to understand the difference between events and delegates in .NET. C# syntax makes them to look deceptively similar:

      MyDelegate a; // delegate
event MyDelegate b; // event

Don't let the syntax fool you. Conceptually, event is a pair of methods: add accessor and remove accessor. Each method takes a single parameter of specific delegate type. Add accessor is invoked via "e+=d" expression, remove accessor is invoked via "e-=d" expression.

Event accessors can be explicitly declared as follows:

class MyClass
{
    public event MyDelegate GoodNews
    {
        add
        {
            // add accessor code here
        }
        
        remove
        {
            // remove accessor code here
        }
     }
}

After seeing full event declaration syntax, it becomes clear that events are more similar to properties than to delegates. Unlike property, event cannot have just one accessor - it must always have both.

Note that event does not provide a "call" or "raise" accessor. This is because entities outside declaring class are not supposed to raise the event. In other words, raising is not part of event's public interface - it is an implementation detail.

Using Events

Users of event register their delegates using operator += and remove them using operator -=. This is all they can do with the event.

Class that implements the event is supposed to invoke registered delegates at specified moments, e.g. when mouse button is down, or when socket data arrives, etc. However, what and when to invoke is entirely up to the author of the class. In particular, there is no guarantee that the class will invoke all the delegates that have been registered. Theoretically, the class can ignore every other delegate, or call only delegates whose target is derived from MarshalByRefObject. Of course, although theoretically possible, in reality such picky classes either don't exist, or are a negligible minority.

Implementing Events

Most typical implementation of an event would be as follows:

class MyClass
{
    public event MyDelegate GoodNews
    {
        add
        {
            lock(this)
            {
                _goodNews += value;
            }
        }
        
        remove
        {
            lock(this)
            {
                _goodNews -= value;
            }
        }
    }
        
    private void OnGoodNews()
    {
        if (_goodNews != null)
        {
            _goodNews();
        }
    }
    
    private MyDelegate _goodNews;
}
Figure 1

Here we use private delegate _goodNews to store invocation list for the event.

This is not the only possible implementation, however. Let's consider a situation when our class defines lots of events, and typically only a couple of events is actually hooked up for a particular instance of the class. In this case, storing lots of private delegates would be a great waste of space. Each instance of the class would carry all the delegates, and only a few of them would be actually used. The solution is to hold in each instance a container that stores only actually used delegates. This is exactly how event-rich UI classes like Control handle their events.

Field-Like Events

Implementation of an event outlined in Figure 1 is by far more common than any other. In fact, it is so common that authors of C# language added a special syntactic shortcut for it. If a class declares a non-abstract event without add and remove accessors, compiler automatically generates default accessors similar to those in Figure 1, and adds an anonymous private delegate field that stores invocation list for the event. Such "default" events are called "field-like events". See section 10.7.1 of C# Language Specification.

In addition, when name of field-like event is used inside the class it refers not to the event itself, but to the anonymous private delegate created by compiler.

class MyClass
{
    public event MyDelegate GoodNews;
        
    private void OnGoodNews()
    {
        if (GoodNews != null)
        {
            GoodNews();
        }
    }
}
Figure 2

Code in Figure 2 is functionally equivalent to code in Figure 1 .

Note, that anonymous delegate is private, and therefore not accessible anywhere, but in the class that declares the event. In particular, the following code does not compile, because derived class does not have access to the anonymous delegate:

class MyClass
{
    public event MyDelegate GoodNews;
}

class Derived : MyClass
{        
    private void OnGoodNews()
    {
        if (GoodNews != null) // bad!
        {
            GoodNews(); // bad!
        }
    }
}

Also, if event is not field like, there is no anonymous delegate that backs it up, and the only things anybody (declaring class included) can do with it are += and -=. For instance, the follownig will not compile:

class MyClass
{
    public event MyDelegate GoodNews
    {
        add
        {
            ...
        }
        
        remove
        {
            ...
        }
    }
        
    private void OnGoodNews()
    {
        if (GoodNews != null) // bad!
        {
            GoodNews(); // bad!
        }
    }
}

Virtual Events

If event is declared with the keyword virtual it makes its acessors virtual. Field-like events can also be virtual. Derived classes can override accessor methods with their own implementation. E.g.

class MyClass
{
    public virtual event MyDelegate GoodNews; // field-like
        
    private void OnGoodNews()
    {
        if (GoodNews != null) 
        {
            GoodNews(); 
        }
    }
}

class Derived : MyClass
{
    public override event MyDelegate GoodNews
    {
        add
        {
            Console.WriteLine("Adding delegate to GoodNews event");
            base.GoodNews += value;
        }
        
        remove
        {
            Console.WriteLine("Removing delegate from GoodNews event");
            base.GoodNews -= value;
        }
    }
}

Event can also be abstract. It means that add and remove accessors are abstract and derived classes must provide their own implementation.

Virtual Events Gotchas

If derived class provides its own implementation of add and remove accessors, and if this implementation puts event invocation list in its own storage, base class has no knowledge of how to invoke the event. Consider the following code:

class MyClass
{
    public virtual event MyDelegate GoodNews; // field-like
        
    private void OnGoodNews()
    {
        if (GoodNews != null) 
        {
            GoodNews(); 
        }
    }
}

class Derived : MyClass
{
    public override event MyDelegate GoodNews
    {
        add 
        { 
            lock(this) 
            { 
                _goodNews += value; 
            } 
        } 
        
        remove 
        { 
            lock(this) 
            { 
                _goodNews -= value; 
            } 
        }     
    }
    
    private delegate MyDelegate _goodNews;
}

Whenever base class invokes the event, it will call only delegates stored in Base.GoodNews. However, GoodNews+=d and GoodNews-=d will oeprate on Derived._goodNews, of which base class has no knowledge.

This is resolved by making function OnGoodNews protected and virtual. If derived class overrides event accessors, it must also override the function that invokes the event.

Summary

All C# delegates implicitly derive from class MulticastDelegate, which is a linked list of target/method pairs. Delegates are compared by logical equality, not by reference equality.

Event is nothing more than a pair of methods. As such, it can be made part of an interface, virtual, abstract, et cetera. "Raise" operation is not part of event behavior. Field-like event is a syntactic shortcut that implements event based on a private anonymous delegate.

Delegates and events are important mechanisms of C# language. Hopefully this article helped to eliminate confusion by demonstrating how things work under the hood.

Great thanks go to Lutz Roeder and his reflector tool.

Feedback

Questions? Comments? Feel free to
Leave feedback