Introduction
Many of us already have some idea of delegates. But let's go deep inside and examine how the CLR/runtime and the compiler interprets the delegates, and what happens when you declare and manipulate delegates.
A delegate is a reference type, thus it does not hold the actual value, rather it holds the address of an object residing somewhere in the heap. To make the understanding of delegates simple, compilers represent the delegates in a different way than how it is interpreted by the CLR/runtime. We will first study how the compiler exposes delegates, and then we will jump into some ground realities of delegates from the CLR/runtime perspective.
Delegates and .NET Compilers
To make life simpler, most .NET compilers symbolize delegates as pointers to a function, just like the function pointers we have in C++. Thus, in C#, you declare delegates as follows:
public delegate void MyDelegate (int someParam, string anotherParam);
From a C# compiler perspective, MyDelagate
is a delegate that could hold the address of a function that has the similar signature.
The ability of holding the address of a function makes delegates ideal for:
- Callback functionality
- Event modeling
- Having more than one implementation against a method
Though the C# compiler exposes delegates as reference types, you could not instantiate the delegate by using the new
operator; rather delegates could be instantiated by:
- Providing the name of the method to the delegate. These types of delegates are known as "Named Delegates".
void DoSomething(int someParam, string anotherParam)
{
}
MyDelegate del = DoSomething;
- Providing the implementation of the method to the delegate. These type of delegates are known as "Anonymous Delegates".
del = delegate(int someParam, string anotherParam)
{
Invoking of named or anonymous delegates is same as calling the function:
Del (29, "some value");
Named Delegates
Named delegates could hold the address of instance methods as well as the address of static methods. If the return type of a method is inheriting from the return type defined in the delegate, then the delegate is known as a Covariance Delegate. If the parameters of the methods are the base types of the parameters defined in the delegate, then the delegate is known as a Contravariance Delegate.
public delegate ICollection MyDelegate (int someParam,
string anotherParam);
IList DoSomething()
{
}
MyDelegate del = DoSomething;
public delegate ICollection MyDelegate (IList param);
IList DoSomething(ICollection param)
{
}
MyDelegate del = DoSomething;
Anonymous Delegates
If an anonymous delegate is referring to some local variables, then the life of that variable would be extended to the life of that delegate.
public delegate void MyDelegate (int someParam, string anotherParam);
MyDelegate del
void DoSomething()
{
int n;
del = delegate(int someParam, string anotherParam)
{
n++;
}
}
Delegates and the CLR/Runtime
.NET compilers have made our life easy in handling delegates. In reality, there is neither any concept of pointers to a function nor any significance to the word "delegate" in the CLR/runtime. Rather, the CLR provides an abstract class System.Delegate
that holds the reference of a static method or class instance and the instance method of that class. The System.Delegate
has two properties.
Target
: Holds the reference of an instance on which the current delegate invokes the instance method. If the method is a static method, this property holds a null
value.
Method
: Holds the instance of the MethodInfo
class for the method represented by the delegate.
The delegate also exposes the DynamicInvoke
function to call the method represented by the Method
property. This function takes an array of objects that are the arguments to pass to the target method. If the target method does not take any arguments, the parameter to the DynamicInvoke
function would be null
.
The CLR also exposes an abstract class System.MultiCastDelegate
that inherits from System.Delegate
. System.MultiCastDelegate
maintains a list, called the Invocation List, of a delegate object. When a multicast delegate is invoked, all the delegates in the invocation list are called synchronously. The System.MultiCastDelegate
exposes the Combine
method to add delegates to the invocation list.
When you define a delegate in C#, a new class with the name of the delegate inheriting from System.MultiCastDelegate
is created by the C# compiler.
public delegate ICollection MyDelegate (IList param);
The above statement causes a a new class to be added in your assembly by the C# compiler.
public class MyDelegate : System.MultiCastDelegate
You can view this class by opening your assembly using the Ildasm utility.
When you assign a method to a delegate, an instance of the compiler generated class is created with the Target
and Method
as parameter to this class. In case the method is static
, null
is passed to the argument, Target
.
As compiler generated classes are inheriting from System.MultiCastDelegate
, thus delegate could hold the addresses of more than one method. C# exposes a simple method to add a method in the invocation list of underlying multiCast delegate object.
MyDelegate del;
del += DoSomething;
del += DoAnotherThing;
Internally, the C# compiler is calls the Combine
method of the compiler-generated delegate class to add the methods to the invocation list.
When you invoke a delegate, C# internally calls the DynamicInvoke
method of the compiler generated class. The compiler generated class also exposes methods to invoke the target methods asynchronously.
A delegate is known as an Open Instance Delegate if the Target
is also provided at the time of invocation.