An Introduction to Delegates | |
Jeffrey Richter | |
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. Using Delegates to Call Static MethodsThe 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 MethodsSo 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 DelegatesOn 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:
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: You could also write code to check if the callback method has a specific name (like FeedbackToMsgBox):
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:
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
but the compiler generates code as though the source code said this:
In fact, you can verify this by using ILDasm.exe to examine the code for the ProcessItems method. ConclusionWell, 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.