Introduction
I could never really find a good tutorial on delegates... Somehow, the discussion always tends to switch over to a talk about events a quarter way through. Delegates themselves can be a powerful tool that can be used to both increase class abstraction and for dynamic method invocation. Before we get to the code, let's first try to create a working definition of what a delegate is.
Delegate: (Let's list a few of the things that come to mind when thinking of delegates.)
- A Type (i.e.
StringBuilder
, DBContext
, List
)
- A method pointer which is type safe (delegate instances store references to methods)
- Looks similar to a method declaration
- The signature of a delegate, however, includes the return type, unlike a method signature which consists of only the method name along with the type and number of parameters.
- Allows methods to be treated as, for the most part, as any other C# object: allowing them to be assigned to other variables and passed as parameters.
So we have: A delegate is a special type (class) that defines a method signature whose instance can be used to reference another method that follows its definition. (This definition will become clearer through examples). To define a delegate, it's actually quite simple. We just add the word "delegate
" with the return type to a typical method signature.
public delegate void ProcessInventoryHandler( String text );
The above code defines a delegate that can reference a method that has one parameter of type String
and returns void
. The following methods can both be referenced using the above delegate:
void AddToInventory( String text )
void FooBar( String foo )
As you can see, the method and the parameter name(s) are not important--just the signature. To think of this in another way, imagine if your boss gives you the company's corporate seal, and requests that you act on her behalf at a contract signing with a new client. It doesn't matter that your boss is a 6 foot 5 inch tall woman that's more than twice your weight and has a deeper voice than you do, the important thing here is that both of you have the same signature (corporate seal). The code definition of delegate is a bit deceiving however, as it looks more like a C/C++ function declaration than a class definition. The CLR does the heavy lifting behind the scenes to convert each delegate to a proper class definition. We can verify this by looking at the MSIL (Microsoft Intermediate Language) code that is generated from the above delegate:
.class public auto ansi sealed MyDotNet.ProcessInventoryHandler
extends [mscorlib]System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname
instance void .ctor (
object 'object',
native int 'method'
) runtime managed
{
}
.method public hidebysig newslot virtual
instance void Invoke (
string text
) runtime managed
{
}
.method public hidebysig newslot virtual
instance class [mscorlib]System.IAsyncResult BeginInvoke (
string text,
class [mscorlib]System.AsyncCallback callback,
object 'object'
) runtime managed
{
}
.method public hidebysig newslot virtual
instance void EndInvoke (
class [mscorlib]System.IAsyncResult result
) runtime managed
{
}
}
Although the details of the MSIL are a bit beyond the scope of this tutorial, we can see here that the ProcessInventoryHandler
delegate is a public sealed
class that inherits from another class called "MulticastDelegate
". Unlike a typical class however, an instance of the delegate class is referred to as a "Delegate
" not as an "Object
." To create a delegate instance, we must first pass the reference of the method we would like to link to the delegate. This is a lot easier than it sounds. As we know, in C#, the most common way to call a method is by using the form: MethodName (int x);
, where "int x
" represents any number of parameters from zero to 2^16 - 1 [mileage may vary, MSIL limits number of args that can be loaded to an unsigned of int16
]. Here the runtime knows that we are making a call to a method because of the open and closing parenthesis. However if we were to write just the method's name, without the parenthesis, or parameters for that matter, we can obtain the needed method reference to pass to the delegate. This method reference is called a Method Group in C#, as a single method name can, but does not necessarily have to, refer to several different method name to method-signature combinations. The CLR will match the delegate type to the appropriate method overload. This matching by the CLR however, does bring us to a few important asides:
- Method Groups have no meaning outside the world of delegates. (As of C# 6)
- The CLR does not like ambiguity when it comes to delegate to method assignments.
To get a better understanding of what I mean by ambiguity in regards to delegate assignments, let's take a look at the following few code samples:
static void Main()
{
var dollarValue = ShowMeTheMoney(new Dollar());
Console.WriteLine ("The dollar amount: {0}{1}", dollarValue.Symbol, dollarValue.Amount);
var wonValue = ShowMeTheMoney(new Won());
Console.WriteLine ("The won amount: {0}{1}", wonValue.Symbol, wonValue.Amount);
}
static Dollar ShowMeTheMoney(Dollar d)
{
d.Amount = 5m;
return d;
}
static Won ShowMeTheMoney(Won w)
{
w.Amount = 5000m;
return w;
}
public class Currency { public decimal Amount {get; set;} }
public class Dollar : Currency
{
private readonly string _symbol = "$";
public string Symbol { get { return _symbol; } }
}
public class Won : Currency
{
private readonly string _symbol = "W";
public string Symbol { get { return _symbol; } }
}
The above code compiles and runs fine. The output is of course:
The dollar amount: $5
The won amount: W5000
Let's now see what happens when we add an additional layer of abstraction to our simple program above, through the use of delegates. For this, we just need to add a delegate declaration and modify the main method as follows (class definitions were left out for brevity):
delegate Currency MoneyHandler(Currency c);
static void Main()
{
MoneyHandler theBank = ShowMeTheMoney;
var dollarValue = theBank(new Dollar());
Console.WriteLine ("The dollar amount: {0}{1}", dollarValue.Symbol, dollarValue.Amount);
var wonValue = theBank(new Won());
Console.WriteLine ("The won amount: {0}{1}", wonValue.Symbol, wonValue.Amount);
}
static Dollar ShowMeTheMoney(Dollar d)
{
d.Amount = 5m;
return d;
}
static Won ShowMeTheMoney(Won w)
{
w.Amount = 5000m;
return w;
}
If we try to compile the above code, the compiler will go BANG [Do not pass go, do not collect $200] and give you the friendly "No overload for 'Method Group' matches delegate 'Delegate Name' " error message. We might try to call foul play and say to ourselves, but wait, both Dollar and Won are derived from Currency, this should work. However, in there lies the problem, that "both" should work. This is just one of the half-dozen-or-so reasons why when the CLR has to match a Method Group to a delegate type, covariance--having a more derived type--for the method parameters is not allowed. However, an important distinction must be made here between the two concepts of ambiguity and multiplicity. In order words, when the method signature is properly defined and distinct, a one-to-many relationship is valid.
delegate Currency MoneyHandler(Currency c);
static void Main()
{
MoneyHandler theBank;
theBank = ShowMeTheMoneyDollar;
theBank += ShowMeTheMoneyWon;
var myCurrency = theBank(new Currency());
}
static Dollar ShowMeTheMoneyDollar(Currency c)
{
c = new Dollar();
c.Amount = 5m;
Console.WriteLine ("The dollar amount: ${0}", c.Amount);
return c as Dollar;
}
static Won ShowMeTheMoneyWon(Currency c)
{
c = new Won();
c.Amount = 5000m;
Console.WriteLine ("The won amount: W{0}", c.Amount);
return c as Won;
}
The output for the above code is the exact same as we had in the simpler non delegate case above, simply:
The dollar amount: $5
The won amount: W5000
The observant eye might also notice something else about the above code, the return types of both methods are derived types of the delegate's return type. In other words, Dollar and Won both inherit from Currency. But wait a minute, didn't I just mention a few paragraphs up on why that was bad? Again, before you cry foul, let's take a look at what's really happening along with a few of the subtle differences in having covariance in the return type verus in the parameters.
- Probably the easiest difference to spot is that now we have two distinct Method Groups, so no ambiguity.
- Return types are by default are "out"s and parameters are by default "in"s. An out type by design is immutable--even though we are returning a more baser type, data integrity is maintained. There is actually no loss in precision as the object never changes, only the reference type. "A rose by any other name..." Well, to be a bit more precise, we are just calling a Rose a Flower.
Well, don't take my word for it, let's add a few lines of code to our main:
delegate Currency MoneyHandler(Currency c);
static void Main()
{
MoneyHandler theBank;
theBank = ShowMeTheMoneyDollar;
theBank += ShowMeTheMoneyWon;
var myCurrency = theBank(new Currency());
Console.WriteLine (((Won)myCurrency).Symbol); Console.WriteLine (myCurrency.GetType()); Console.WriteLine (((Dollar)myCurrency).Symbol); }
[*Note: The compiler would go BANG if we tried to do this with value types, such as double
and float
: there is no reference to pass].
The Object
class contains a "GetType
" method that returns an object's type. Since all object instances are derived from Object
, we can call the GetType
method on 'myCurrency
' and see that it's in fact the derived, 'Won
'. If we try to cast 'myCurrency
' to 'Dollar
', for example, an error is thrown. Now let's get to our final topic for delegates, and that is, besides events, so why should we use them? The best way to answer that might possibly be by using the following, albeit over simplified, analogy: class is to interface as method is to...delegate. In-fact, many delegate-method use cases can be replaced with an equivalent interface-class implementation. But to do so would create wordy code, decrease readability, and opens you up to the potential for unwanted "side-effects."