How do C# delegates in .NET work under the hood?

A

anonieko

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:

Class Delegate.
Class MulticastDelegate.
Field-like events.
Event accessors.
Virtual events.
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: invokaction code first invokes Previous
delegate (which recursively invokes the whone chain), and that 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, which 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.
(http://ikriv.com:8765/en/prog/info/dotnet/Delegates.html)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top