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 Method
s 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 Method
s 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; |
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; } |
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(); } } } |
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
Programming Tools & Info | Copyright (c) Ivan Krivyakov. Last updated: February 19, 2005 |