| Internals of Delegate chaining |
| By Aarthi Saravanakumar |
Download C# Source Code
|
| Introduction to Delegate Chaining |
|
Anyone who has written a simple .aspx page or a Windows form that has any kind
of user interaction has already worked with delegates. You may not have
realised it, but you have also used delegate chaining. Surprised? Ever seen the
+= in C# or AddHandler in VB.NET used with events? That is VS .NET generating
delegate chains for you! This article aims at covering the
internals of delegate chaining in .NET 1.1. I hope at the end of it, you will
be able to write a += for delegates in c# or AddHandler in VB.NET with a
lot more understanding.
|
|
An inside look at Delegates
|
|
Before we look more into delegate chaining, let us look at a class that has a
delegate. Nothing very fancy, a simple class that has a delegate named
InterestingSubs and a method named TriggerEvent, that checks if the delegate
has any registered methods(not null) and if so calls those methods with a
string argument of "Something Very Interesting'.
|
public class InterestingInfo
{
public delegate void InterestingSubs(string info);
public static void TriggerEvent(InterestingSubs subs)
{
if(subs !=null)
{
subs("Something very interesting");
}
}
}
|
|
|
|
Let us now compile this exe and use ILDASM to look at the result..and what do
we see? We see a class named InterestingSubs..but didn't we declare
InterstingSubs as a delegate? Yes, we did. But when the C# compiler comes
across the delegate keyword, it generates a class of the same name with a
method named Invoke with the same signature of the delegate.So it does not
come as much of a surprise that our subs("Something very interesting");
gets translated to DelegatesSample.InterestingInfo/InterestingSubs::Invoke(string),
a method call to the auto-generated "Invoke". Also notice one other thing. The
class InterestingInfo derives from System.MulticastDelegate. Why is this
significant? Because it is MulticastDelegate has all the hidden magic to make
delegate chains possible!
|
|
| Delegate and MulticastDelegate |
|
There are two flavors of delegates, at least there were when .NET framework
started out. (Many predicted that this was an oversight and that
in version 2.0, we would have just one version namely MulticastDelegate.
But by the look of the betas of 2.0, looks like Microsoft carried over
System.Delegate .) There were one set of delegates that were supposed
to be used when return values were expected from the delegate call, and when
multiple methods were not invoked as part of the delegate invocation.This
started out as System.Delegate. Then came about System.MulticastDelegate. This
class inherits from System.Delegate and was meant for use when no return values
were expected and when multiple delegate calls were to be chained together. But
then System.Delegate well on the wayside and all delegate declarations started
inheriting from System.MulticastDelegate.So now that we have talked about these
two classes, let us see what the structure of these two classes look like. I
have omitted some fields for clarity's sake
|
public abstract class Delegate:ISerialzable,ICloneable
{
public static Delegate Combine(Delegate[] delegates);
public static Delegate Combine(Delegate a,Delegate b);
public static Delegate Remove(Delegate source, Delegate value);
public static Delegate RemoveAll(Delegate source, Delegate value);
public override bool Equals(object obj);
private IntPtr _methodPtr;
private object _target;
}
|
|
Here _methodPtr is the pointer to the method that needs to be called by the
delegate and the _target is the object that contains the method. This is null,
if the delegate maps to a static method, and contains a reference to the object
for an instance method.
The interesting methods that you need make a note of are the Combine
overrides. Just remember that in the override that takes two delegates, "a" is
the chain to which "b" should be added to.We will look deeper into this in
the delegate chaining section. Now that we have talked of System.Delegate, what
does the Multicast delegate comprise of? A lot of fields but most importantly a
field named "_prev" because of which delegate chaining is possible.
|
| Delegate Chaining |
|
Before we look into how delegate chains are implemented, let us look at what a
delegate chain's C# code looks like
|
class Subscriber
{
public static void IgnoreEvent(string info)
{
//Ignore the event
Console.WriteLine("SOMETHING IMPORTANT IGNORED " + info);
}
public void PublishEventOnWeb (string info)
{
//Code to publish event on web
Console.WriteLine("SOMETHING IMPORTANT ON WEB " + info);
}
public void FaxEvent (string info)
{
//Code to publish event on web
Console.WriteLine("SOMETHING IMPORTANT FAXED " + info);
}
}
class TestDelegates
{
static void Main(string[] args)
{
Subscriber sub = new Subscriber();
InterestingInfo.InterestingSubs a = new DelegatesSample.InterestingInfo.InterestingSubs(Subscriber.IgnoreEvent);
InterestingInfo.InterestingSubs b =new DelegatesSample.InterestingInfo.InterestingSubs(sub.PublishEventOnWeb);
InterestingInfo.InterestingSubs c =new DelegatesSample.InterestingInfo.InterestingSubs(sub.FaxEvent);
InterestingInfo.InterestingSubs subs =a;
subs+=b;
subs+=c;
InterestingInfo.TriggerEvent(subs);
Console.ReadLine();
}
}
|
|
A fairly straight-forward Subscriber class that has three methods (one static
and two instance) that match the signature of the delegate InterestingSubs. The
TestDelegates class just creates an instance of Subscribe and attaches the
subscriber's methods to the delegate and calls TriggerEvent. The point of
interest here is subs+=new
InterestingInfo.InterestingSubs(sub.PublishEventOnWeb); Let us compile this
executable and look at the Main method using
Lutz's reflector
|
private static void Main(string[] args)
{
Subscriber subscriber1 = new Subscriber();
InterestingInfo.InterestingSubs subs1 = new InterestingInfo.InterestingSubs(Subscriber.IgnoreEvent);
InterestingInfo.InterestingSubs subs2 = new InterestingInfo.InterestingSubs(subscriber1.PublishEventOnWeb);
InterestingInfo.InterestingSubs subs3 = new InterestingInfo.InterestingSubs(subscriber1.FaxEvent);
InterestingInfo.InterestingSubs subs4 = subs1;
subs4 = (InterestingInfo.InterestingSubs) Delegate.Combine(subs4, subs2);
subs4 = (InterestingInfo.InterestingSubs) Delegate.Combine(subs4, subs3);
InterestingInfo.TriggerEvent(subs4);
Console.ReadLine();
}
|
|
See how the line subs+=new
InterestingInfo.InterestingSubs(sub.PublishEventOnWeb); in our
code got translated to a call to Delegate.Combine(subs1, new
InterestingInfo.InterestingSubs(subscriber1.PublishEventOnWeb)); So
looks like Delegate.Combine is doing all the hard work of implementing the
delegate chain, doesn't it? So how does this delegate chain look like?
|
| Delegate.Combine |
|
The twist lies in the fact that Delegate class if you remember was not really
designed for Multicasting delegates. It was designed for single delegates, so
why are we calling a static method in Delegate class to do our chaining? The
Delegate.Combine method does two things:one: checks if the types of the
Delegates being passed in match, and if not throws an exception. The second
thing that it does is calls a protected virtual method named CombineImpl
(that is overriden in MulticastDelegate). Now remember that all our delegates
derive from System.MulticastDelegate , so because of runtime polymorphism, the
CombineImpl in MulticastDelegate gets called.
Also if you remember(or scroll up) you will see that Delegate.Combine
takes two "System.Delegate" s as parameter ( Not
"System.MulticastDelegate"). Because of this, when CombineImpl in
MulticastDelegate is called, the method parameter passed to it is also of type
Delegate.Now, we know that System.Delegate class has no functionality that will
help us in chaining. But wait, MulticastDelegate on the other hand has the
pointer _prev. So the first step is to clone the delegate b and cast this to a
MulticastDelegate type. Now, we can access its _prev field. A mention about the
_prev field. This contains the reference to the next delegate in the chain. So
the first delegate added has _prev as null. When the next delegate gets added,
it is cloned internally into a MulticastDelegate and its _prev pointer is fixed
up to point to the first delegate added and so on. The following diagram better
explains this. The small yellow boxes are (arbitrary) memory addresses where
the objects are created. The green ones denote the _prev pointer.
|
|
|
The pointer 4010 is what gets returned as the return parameter of
Delegate.Combine.As you would have realized, a delegate chain is internally
implemented as a linked list with the the first delegate added to be the one
with the _prev pointer as null and the last delegate added being at the head of
the list.So if a,b and c are delgates added to a chain as per the code shown
above , the chain looks like the following figure.
|
|
|
Just a mention of the other Delegate.Combine method that takes an array of
delegates. This overload, internally calls the Delegate.Combine overload that
takes two delegates (which we just examined )for all the elements of the array.
So the previous example can be re-written as below and the delegates will be
called in the order a,b.c and would be structured the same as the diagram
above.
|
Delegate[] dels= new Delegate[3];
dels[0]=a;
dels[1]=b;
dels[2]=c;
InterestingInfo.InterestingSubs subs1=(InterestingInfo.InterestingSubs)Delegate.Combine(dels);
InterestingInfo.TriggerEvent(subs1);
|
|
Delegate.Remove |
|
This article would not be complete without a small discussion on
Delegate.Remove. First, let us talk a little bit about the Object.Equals()
method that System.Delegate overrides. This implementation of Equals
considers two delegates as equal if they have the same _target and _methodPtr.
Remember that _target points to the object that contains the method to be
called and is null for static methods. _methodPtr is the pointer to the
method in the object. So the code below, will display "a1 equals b1: true"
|
InterestingInfo.InterestingSubs a1 =new DelegatesSample.InterestingInfo.InterestingSubs(Subscriber.IgnoreEvent);
InterestingInfo.InterestingSubs b1 =new DelegatesSample.InterestingInfo.InterestingSubs(Subscriber.IgnoreEvent);
Console.WriteLine("a1 equals b1 : " + Delegate.Equals(a1,b1));
Console.ReadLine();
|
|
A warning: I am oversimplifying Delegate.Remove here. When you call
Delegate.Remove(a,b) or use -= ,what happens is very similar to the chain of
events that occur for Delegate.Combine. The _prev pointers are re-arranged so
that delegate "b" is removed from the calling tree. The reason we covered
Equals is that, Delegate.Remove uses this Equals implementation when it
tries to remove a delegate. For example: the below two pieces of code produce
the same results.
|
//Using Same Delegate Instance
InterestingInfo.InterestingSubs a =new DelegatesSample.InterestingInfo.InterestingSubs(Subscriber.IgnoreEvent);
InterestingInfo.InterestingSubs b =new DelegatesSample.InterestingInfo.InterestingSubs(sub.PublishEventOnWeb);
InterestingInfo.InterestingSubs c =new DelegatesSample.InterestingInfo.InterestingSubs(sub.FaxEvent);
InterestingInfo.InterestingSubs subs =a;
subs+=b;
subs+=c;
InterestingInfo.TriggerEvent(subs);
Console.WriteLine("*************REMOVING ONE DELEGATE*****************");
subs-=b;
InterestingInfo.TriggerEvent(subs);
//Demonstrate Remove with a new delegate instance
Console.WriteLine("*************REMOVING DELEGATE USING A DIFFERENT DELEGATE INSTANCE*****************");
InterestingInfo.InterestingSubs a4 =new DelegatesSample.InterestingInfo.InterestingSubs(Subscriber.IgnoreEvent);
InterestingInfo.InterestingSubs b4 =new DelegatesSample.InterestingInfo.InterestingSubs(sub.PublishEventOnWeb);
InterestingInfo.InterestingSubs c4 =new DelegatesSample.InterestingInfo.InterestingSubs(sub.FaxEvent);
InterestingInfo.InterestingSubs subs6=a4;
subs6+=b4;
subs6+=c4;
Console.WriteLine("Before removing a4");
InterestingInfo.TriggerEvent(subs6);
InterestingInfo.InterestingSubs b5 =new DelegatesSample.InterestingInfo.InterestingSubs(sub.PublishEventOnWeb);
subs6-=b5;
Console.WriteLine("After removing a4");
InterestingInfo.TriggerEvent(subs6);
|
|
Summary |
|
In this article, we have taken a brief look at Delegates and concentrated on
Delegate chaining in .NET 1.1 framework. Some things have changed with .NET 2.0
so far as delegates are concerned. So we will cover those in a future article.
If this article has whetted your appetite for delegates, I would suggest
reading Jeff Ritcher's articles on the same and using Lutz
Roeder's Reflector and exploring these classes on your own.
|