Q. In .NET, what is a delegate? When would I want to use them? How are they best implemented? What are the "gotchas"?
A. Delegates are fun. If you've been programming for any length of time, you've been introduced to the concept of delegates as function pointers. As a quick review, pointers are used to store the address of a thing. By changing the address contained in a pointer, the same pointer can reference multiple things during the course of execution of a program. Thus, a pointer named "stackTop" can point to an infinite number of things as they are pushed and popped from a stack implementation. We are most familiar with pointers pointing to pieces of information. However, since pointers just store addresses under the covers, they can point to other program data as well. In particular a pointer can store the location of a function entry point and a programmer can use the pointer to instruct a computer to execute at the location stored in the pointer. Typically, the role of telling the computer how to hop around is relegated to the compiler and all the jumps are determined before the program starts running. However, it is sometimes not possible to know all the jumps when a program is compiled and the jumps need to be determined by a program as it executes. This is where we typically use function pointers.
I've said more about pointers than most folks need, I'm sure.
Delegates behave similarly to function pointers in C and C++. In C a programmer can create a type-safe pointer to a function and store those pointers just like any other pointer. These pointers can be assigned and later used to call the functions they reference. In C++ and other object languages, things get a bit more tricky. In object-oriented languages, most function processing is done in the context of an object instance (method call). Consider the case where a class implements a generic print function that takes no arguments and returns void. Let's consider further that calling the print function on an object instance emits the object's state information to the console. Now let's consider that we create a pointer to the print function where the pointer's definition is "a function that takes no arguments and returns void". Now we go along and tell the computer to jump to this function pointer. At jump time, the only information available to the computer is the jump location in our definition. The problem is that to print the state information out for the object, you need the object!! In fact, if you go and muck about in the code generated by a C++ compiler, you will see that every (non static) method call includes a hidden argument which is a pointer to the object instance that the method is being called upon. So, to represent a pointer to a method in a particular object, you need not one but two pieces of information, the location where the computer is supposed to jump to and the object that the method is supposed to work on. The implementers of the C++ language chose to dodge implementing instances of pointers to member functions (note that I said instances of pointers to members...) deciding, in a nutshell, that it opened both a philosophical and practical can of worms.
C# has embraces the worms and implements a mechanism for creating pointers to specific functions on specific objects. Given all the introductory blather above, you should be able to guess pretty much what they do. Here are the important bits. Delegates encapsulate both the pointer to the method and the object context of the method (i.e., pointer to the object), where latter is never exposed to the programmer. In addition, and this is particularly interesting, a delegate can be assigned any other delegate where the method signatures match. At first blush this may not seem too interesting but consider an object that delegates its print method. For arguments sake, let's assume the object class is a "Document" class. Let's also assume that the class exposes a public delegate that can be assigned to print methods. When the document's print method is called, it calls the delegate to do its dirty work. Now let's assume we have a "CanOpener" class that also implements a print method that has the same signature as the delegate to which document delegates its printing. We can create a delegate from the CanOpener print method and assign that to the Document's print delegate! Woohoo! Lots of fun!
Here is some code that might make things clearer.
using System;
namespace delegate_test
{
//This defines a printing delegate type.
//It takes no arguments and returns void.
public delegate void PrintFunction();
class DelegateTest
{
[STAThread]
static void Main(string[] args)
{
//This test class creates a JimDoc and
//two helper classes. Each helper class is
//used to create delegates that are in turn
//assigned to the print delegate of the JimDoc
//instance.
//The objects
JimDoc theDoc = new JimDoc();
DocPrinter thePrinter = new DocPrinter();
CanOpener theCanOpener = new CanOpener();
//First test with no assigned printer
theDoc.Print();
//Now assigng a printer function and test
theDoc.m_PrintFunction =
new PrintFunction(thePrinter.Print);
theDoc.Print();
//Now re-assign the printer function and test
//again
theDoc.m_PrintFunction =
new PrintFunction(theCanOpener.Print);
theDoc.Print();
}
}
public class JimDoc
{
//The print delegate member
public PrintFunction m_PrintFunction;
public JimDoc()
{
}
public void Print()
{
//Printing invokes the delegate or
//displays an error.
if(m_PrintFunction != null)
m_PrintFunction();
else
Console.WriteLine("No printer assigned");
}
}
public class DocPrinter
{
public DocPrinter()
{}
public void Print()
{
Console.WriteLine("DocumentPrinter Print() called!");
}
}
public class CanOpener
{
public CanOpener()
{}
public void Print()
{
Console.WriteLine("CanOpener Print() method called");
}
}
}
The output generated by this code is shown below
No printer assigned
DocumentPrinter Print() called!
CanOpener Print() method called
This is a toy example of course. A more common use of delegates is when an object implements a callback mechanism that others can "register" with. Implementing event notification registration typically entails having your callback placed on a list of callback methods that get invoked when an event occurs. When events occur, the "event generating" object calls all the callbacks in its callback list. The event generating object doesn't care about any details of the called object other than the signature of its callback.
Given that one of the uses for delegates is to support event handling, a feature was added to delegates to encapsulate this "list of callback functions" idiom. Delegates implement a capability that allows multiple delegates to be combined into a single instance. Invoking this composite delegate causes each of the composite members to be invoked. The "+", "-", "+=" and "-=" operators are overloaded to support delegate aggregation. The process of invoking a composite delegate and having the individual delegates invoked is often referred to "multicasting".
Consider the following implementation of the Main() method from our example above (previous comments stripped).
static void Main(string[] args)
{
DelegateTest tester = new DelegateTest();
JimDoc theDoc = new JimDoc();
DocPrinter thePrinter = new DocPrinter();
CanOpener theCanOpener = new CanOpener();
theDoc.Print();
theDoc.m_PrintFunction =
new PrintFunction(thePrinter.Print);
theDoc.Print();
//Now combine the first delegate with
//the second one instead of replacing it.
theDoc.m_PrintFunction +=
new PrintFunction(theCanOpener.Print);
theDoc.Print();
}
The output from this would be:
No printer assigned
DocumentPrinter Print() called!
DocumentPrinter Print() called!
CanOpener Print() method called
You can see here that the first and second invocations create the same output (lines 1 & 2). The last two lines however, are the result of multicasting the invocation of the print delegate to the combination of the delegates created from both the JimDoc and DocumentPrinter print function delegates.
Finally, for your added benefit, we'll throw in the Ginsu-knives and some additional information about events. Events are a special qualification that can be applied to delegates. Declaring a delegate to be an event restricts the operations allowed on the event delegate outside of the context of the enclosing class. It is only possible to invoke the "+=" and "-=" operators on an event. What this means is that a class can expose a delegate to the outside world for event handling but restrict external entities from invoking (i.e., firing) the event.