MSDN Magazine > Issues and Downloads > 2001 > April >  .NET: An Introduction to Delegates
An Introduction to Delegates
Jeffrey Richter
C
allback functions are certainly one of the most useful programming mechanisms ever created. The C runtime's qsort function takes a callback function to sort elements within an array. In Windows, callback functions are required for window procedures, hook procedures, asynchronous procedure calls, and more. In the Microsoft® .NET Framework, callback methods are used for a whole slew of things. You can register callback methods to get assembly load/unload notifications, unhandled exception notifications, database/window state change notifications, file system change notifications, menu item selections, completed asynchronous operation notifications, filtering a set of items, and so on.
      In C/C++, the address of a function is just a memory address. This address doesn't carry along any additional information such as the number of parameters expected by the function, the types of these parameters, the function's return value type, and the function's calling convention. In short, C/C++ callback functions are not type-safe.
      In the .NET Framework, callback functions are just as useful and as pervasive as they are in unmanaged programming for Windows®. However, the .NET Framework has the added bonus of providing a type-safe mechanism called delegates. I'll begin my discussion of delegates by examining how to use one. The code in Figure 1 shows how to declare, create, and use delegates.
      Notice the Set class at the top of Figure 1. Pretend that this class contains a set of items that will be processed individually. When you create a Set object, you pass the number of items it should manage to its constructor. The constructor then creates an array of Objects and initializes each to an integer value.
      The Set class also defines a public delegate. The delegate indicates the signature of a callback method. In this example, a Feedback delegate identifies a method that takes three parameters (an Object, an Int32, and another Int32) and returns void. In a way, a delegate is very much like a C/C++ typedef that represents the address of a function.
      In addition, the Set class defines a public method called ProcessItems. This method takes one parameter, feedback, which is a reference to a Feedback delegate object. ProcessItems iterates through all the elements of the array, and for each element the callback method (specified by the feedback variable) is called. This callback method is passed the value of the item being processed, the item number, and the total number of items in the array. The callback method can process each item any way it chooses.

Using Delegates to Call Static Methods

      The StaticCallbacks method demonstrates the various ways of using callback delegates. This method begins by constructing a Set object, telling it to create an array of five objects. ProcessItems is called, passing it null for its feedback parameter. This is the first example of how to use delegates. ProcessItems represents a method that performs some action for every item managed by Set. Since the feedback parameter is null in this example, each item is processed without calling any callback methods.
      For the second example, a new Set.Feedback delegate object is constructed. This delegate object is a wrapper around a method, allowing that method to be called back indirectly via the wrapper. To the Feedback type's constructor, the name of a method, App.FeedbackToConsole in this example, is passed; this indicates the method to be wrapped. The reference returned from the new operator is then passed to ProcessItems. Now, when ProcessItems executes, it will call the App type's FeedbackToConsole method for each item in the set. FeedbackToConsole simply writes a string to the console indicating which item is being processed and the value of that item.
      The third example is really identical to the second one. The only difference is that the Feedback delegate object wraps the App.FeedbackToMsgBox method. FeedbackToMsgBox builds a string indicating which item is being processed and the value of that item. This string is then displayed in a message box.
      The fourth and final example demonstrates how delegates can be linked together to form a chain. In this example, a reference variable to a Feedback delegate object, fb, is created and initialized to null. This variable points to the head of a linked list of delegates. A value of null indicates that there are no nodes in the linked list. Then, a Feedback delegate object is constructed that wraps a call to App's FeedbackToConsole method. The C# += operator is used to append this object to the linked list referred to by fb. The fb variable now refers to the head of the linked list.
      Finally, another Feedback delegate object is constructed that wraps a call to App's FeedbackToMsgBox method. Again, the C# += operator is used to append this object to the linked list, and fb is updated to refer to the new head of the linked list. Now, when ProcessItems is called, it is passed the head of the linked list of Feedback delegates. Inside ProcessItems, the line of code that calls the callback method actually ends up calling all of the callback methods wrapped by the delegate objects in the linked list. In other words, for each item being iterated, FeedbackToConsole will be called, immediately followed by FeedbackToMsgBox. I will explain exactly how delegate chain works in my next column.
      It is important to note that everything is type-safe in this example. For instance, when constructing a Feedback delegate object, the compiler ensures that App's FeedbackToConsole and FeedbackToMsgBox methods have the exact prototype, as defined by the Feedback delegate. That is, both methods must take three parameters (Object, Int32, and Int32) and both methods must have the same return type (void). If the method prototypes don't match, then the compiler will issue the following error message: "error CS0123: The signature of method 'App.FeedbackToMsgBox()' does not match this delegate type."

Calling Instance Methods

      So far I've discussed how to use delegates to call static methods. However, delegates can also be used to call instance methods for a specific object. For instance methods, the delegate needs to know the instance of the object that is to be operated on by the method.
      To understand how calling back an instance method works, take a look at the InstanceCallbacks method in Figure 1. This code is extremely similar to the code for static methods. Note that after the Set object is constructed, an App object is constructed. This App object doesn't have any fields or properties associated with it and is created merely for demonstration purposes. When the new Feedback delegate object is constructed, its constructor is passed appobj.FeedbackToFile. This causes the delegate to wrap a reference to the FeedbackToFile method, which is an instance method (not static). When this instance method is called, the object referred to by appobj is the object being operated on (passed as the hidden this parameter). The FeedbackToFile method works like the FeedbackToConsole and FeedbackToMsgBox methods except that it opens a file and appends the processing item string to the end of the file.

Demystifying Delegates

      On the surface, delegates seem really easy to use: you define them using the C# delegate keyword, you construct instances of them using the familiar new operator, and you invoke the callback using familiar method call syntax (except instead of a method name, you use the variable that refers to the delegate object).
      However, what's really going on is quite a bit more complex than what the previous samples illustrate. The compilers and the common language runtime (CLR) do a lot of behind-the-scenes processing to hide this additional complexity. In this section, I'll focus on how the compiler and CLR work together to implement delegates. This knowledge will greatly improve your understanding of delegates and will teach you how to use them efficiently and effectively. I'll also touch on some additional features that delegates make available to you in your code.
      Let's start by reexamining this line of code:
public delegate void Feedback(
   Object value, Int32 item, Int32 numItems);
When the compiler sees the previous line, it actually defines a complete class definition that looks something like the code in Figure 2.
      In fact, you can verify that the compiler did indeed generate this class automatically by examining the resulting module with ILDasm.exe (see Figure 3).
      In this example, the compiler has defined a class called Feedback that is derived from the System.MulticastDelegate type, which is defined in the Framework Class Library. Be aware that all delegate types are derived from MulticastDelegate. Note that in this example the Feedback class is public because, in the source code, the delegate is declared as public. If the source had indicated private or protected, then the Feedback class generated by the compiler would also be private or protected. You should be aware that delegate types may be defined within a class (as in the example, Feedback is defined within the Set class); delegates may also be defined at global scope. Basically, since delegates are classes, a delegate may be defined anywhere a class may be defined.
      Since all delegate types are derived from MulticastDelegate, they inherit MulticastDelegate's fields, properties, and methods. Of all these members, there are three private fields that you should be aware of (see Figure 4).
      Notice that all delegates have a constructor that takes two parameters: a reference to an object and an integer that refers to the callback method. However, if you examine the source code, you'll see that you are passing in values such as App.FeedbackToConsole or appobj.FeedbackToFile. All of your sensibilities tell you that the code should not compile!
      However, the compiler knows that a delegate is being constructed, and the compiler parses the source code to determine which object and method are being referred to. A reference to the object is passed for the target parameter, and a special Int32 value (obtained from a MethodDef or MethodRef metadata token) that identifies the method is passed for the methodPtr parameter. For static methods, null is passed for the target parameter. Inside the constructor, these two parameters are saved in their corresponding private fields.
      In addition, the constructor sets the field to null. This field is used to create a linked list of MulticastDelegate objects. I'll ignore the _prev field for now and discuss it in more detail in the next .NET column.
      So, each delegate object is really a wrapper around a method and an object to be operated on when the method is called. The MulticastDelegate class defines two read-only public instance properties: Target and Method. Given a reference to a delegate object, you can query these properties. The Target property returns a reference to the object that will be operated on if the method were called back. If the method is static, then Target returns null. The Method property returns a System.Reflection.MethodInfo object that identifies the callback method.
      There are several ways that you could use this information. One way is to check to see if a delegate object refers to an instance method of a specific type:
Boolean DelegateRefersToInstanceMethodOfType(
   MulticastDelegate d, Type type) {

   return((d.Target != null) && d.Target.GetType == type);
}
You could also write code to check if the callback method has a specific name (like FeedbackToMsgBox):
Boolean DelegateRefersToMethodOfName(
   MulticastDelegate d, String methodName) {

   return(d.Method.Name == methodName);
}
      Now that you know how delegate objects are constructed, let's talk about how the callback method is actually invoked. For convenience, the code to Set's ProcessItems is repeated here:
public void ProcessItems(Feedback feedback) {
   for (Int32 item = 1; item <= items.Length; item++) {
      if (feedback != null) {
         // If any callbacks are specified, call them
         feedback(items[item], item, items.Length);
      }
   }
}
Just below the comment is the line of code that invokes the callback method. On careful inspection, it appears that I'm actually calling a function called feedback and I'm passing the function three parameters. However, there is no function called feedback. Again, the compiler knows that feedback is a variable that refers to a delegate object, and the compiler actually generates code to call the delegate object's Invoke method. In other words, the compiler sees this
feedback(items[item], item, items.Length);
but the compiler generates code as though the source code said this:
feedback.Invoke(items[item], item, items.Length);
In fact, you can verify this by using ILDasm.exe to examine the code for the ProcessItems method.
      Figure 5 shows the Microsoft intermediate language for the Set type's ProcessItems method. The red arrow points to the instruction that calls Set.Feedback's Invoke method. If you were to modify the source code to call Invoke explicitly, the C# compiler would complain and produce the following error message: "error CS1533: Invoke cannot be called directly on a delegate." C# doesn't allow you to call Invoke explicitly (however, a different compiler might).

Figure 5 SetProcessItems (IL disassembly)
Figure 5 SetProcessItems (IL disassembly)

      You'll recall that the compiler defined the Invoke method when it defined the Feedback class. When Invoke is called, it uses the private _target and _methodPtr fields to call the desired method on the specified object. Note that the signature of the Invoke method matches the signature of the delegate exactly. That is, since my Feedback delegate took three parameters and returned void, the Invoke method takes the same three parameters and returns void as well.

Conclusion

      Well, it seems that I've run out of room in this column to tell you any more about delegates. But, at this point, you certainly know enough to create and use them. In my next column, I'll explain linked list delegate chains, some additional methods on MulticastDelegate, the System.Delegate type, and events. Until then, don't call me; I'll call you!
Jeffrey Richter is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999), and is a cofounder of Wintellect (http://www.Wintellect.com), a software education, debugging, and consulting firm. He specializes in programming/design for .NET and Win32. Jeff is currently writing a Microsoft .NET Framework programming book and offers .NET technology seminars.

From the April 2001 issue of MSDN Magazine.

Page view tracker