Share via


From the June 2001 issue of MSDN Magazine

MSDN Magazine

Delegates, Part 2
Jeffrey Richter
T

wo months ago (April 2001) I introduced the Microsoft® .NET Framework�s version of callback methods: delegates. In that column I explained how declaring a delegate caused the compiler to generate a class derived from System.MulticastDelegate and how each instance of this class contains two private fields (_target and _methodPtr) that indicate which object the callback method should operate upon. I also introduced a third private field, _prev, which is used to maintain a linked-list chain of delegates. In this month�s column, I will focus on this _prev field and how linked-list delegate chains are managed and used.

Some Delegate History
(System.Delegate and System.MulticastDelegate)

      The .NET Framework Class Library defines the System.MulticastDelegate class. This is the class that I discussed in my April 2001 column. However, you should be aware that MulticastDelegate is actually derived from the System.Delegate class (also defined in the .NET Framework Class Library), which itself is derived from System.Object.
      When originally designing the .NET Framework, Microsoft engineers felt the need to provide two different types of delegates: single-cast and multicast. MulticastDelegate-derived types would represent delegate objects that could be chained together and Delegate-derived types would represent objects that could not be chained together. The System.Delegate type was designed as the base type, and this class implemented all of the functionality necessary to call back a wrapped method. The MulticastDelegate class was derived from the Delegate class and added the ability to create a linked list (or chain) of MulticastDelegate objects.
      When compiling source code, a compiler would check the delegate�s signature and select the more appropriate of the two classes for the compiler-generated delegate type�s base class. For the curious, methods with a signature that indicated a void return value would be derived from System.Delegate, while methods with a non-void return value would be derived from System.MulticastDelegate. This made sense since you can only get the return value from the last method called in a linked-list chain.
      During the beta testing of the .NET Framework, it became clear that having the two different base types confused developers. In addition, designing delegates this way placed arbitrary limitations on them. For example, many methods have return values that, in many situations, can be ignored. Since these methods would have a non-void return value, they wouldn�t be derived from the MulticastDelegate class, preventing them from being combined into a linked-list.
      To reduce developer confusion, Microsoft engineers wanted to merge the Delegate and MulticastDelegate classes together into a single class that allowed any delegate object to participate in a linked-list chain. All compilers would generate delegate classes deriving from this one class. This change would reduce complexity and effort for the .NET Framework team, the common language runtime (CLR) team, the compiler teams, and for third-party developers in the field who are using delegates.
      Unfortunately, the idea of merging the Delegate and MulticastDelegate classes came along a bit late in the .NET Framework development cycle and Microsoft was concerned about the potential bugs and testing hit that would occur if they actually made all the necessary changes. So, in Beta 1 of the .NET Framework, these classes have not been merged. You should certainly expect them to be merged into a single class in a future version of the .NET Framework.
      While Microsoft chose to delay the merging of these two classes in the .NET Framework Class Library, they did modify all of the Microsoft compilers so that they now generate delegate types derived from the MulticastDelegate class all the time. So, in my last column when I said that all delegate types are derived from MulticastDelegate, I was not lying. Because of this change to the compiler, all instances of delegate types can be combined into a linked-list chain regardless of the callback method�s return value.
      So why do you need to understand all of this? As you start working more and more with delegates, you will certainly run across both the Delegate and MulticastDelegate types in the .NET Framework SDK documentation. I wanted you to understand the relationship between these two classes. In addition, even though all delegate types you create have MulticastDelegate as a base class, you will occasionally manipulate your types using methods defined by the Delegate class instead of the MulticastDelegate class.
      For example, the Delegate class has static methods called Combine and Remove. (I�ll explain what these methods do later.) The signatures for both of these methods indicate that they take Delegate parameters. Since your delegate type is derived from MulticastDelegate, which, in turn, is derived from Delegate, instances of your delegate type may be passed to the Combine and Remove methods.

Comparing Delegates for Equality

      The Delegate base class overrides Object�s virtual Equals method. The MulticastDelegate type inherits Delegate�s implementation of Equals. Delegate�s implementation of Equals compares two delegate objects to see if their _target and _methodPtr fields refer to the same object and method. If these two fields match, then Equals returns true; otherwise Equals returns false. The following code demonstrates this:

  // Construct 2 delegate objects that refer to the 
// same target/method
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToConsole);

// Even though fb1 and fb2 refer to two different objects
// internally, they both refer to the same callback
// target/method.
Console.WriteLine(fb1.Equals(fb2)); // Displays "True"

 

      In addition, both the Delegate and MulticastDelegate types provide overloads for the equality (==) and inequality (!=) operators. Therefore, you can use these operators instead of calling the Equals method. The following code is identical to that shown above:

  // Construct 2 delegate objects that refer to the same target/method
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToConsole);

// Even though fb1 and fb2 refer to two different objects internally,
// they both refer to the same callback target/method.
Console.WriteLine(fb1 == fb2); // Displays "True"

 

      Understanding how to compare delegates for equality is important when you try to manipulate delegate chains, as discussed in the next section.

Delegate Chains

      By themselves, delegates are incredibly useful. But delegates also support chaining, which makes them even more useful. In my last column, I mentioned that each MulticastDelegate object has a private field called _prev. This field holds a reference to another MulticastDelegate object. That is, every object of type MulticastDelegate (or any type derived from MulticastDelegate) has a reference to another MulticastDelegate-derived object. This field allows delegate objects to be part of a linked-list.
      The Delegate class defines three static methods that you can use to manipulate a linked-list chain of delegate objects:

  class System.Delegate {
// Combines the chains represented by head & tail, head is returned
// (NOTE: head will be the last delegate called)
public static Delegate Combine(Delegate tail, Delegate head);

// Creates a chain represented by the array of delegates
// (NOTE: entry 0 is the head and will be the last delegate called)
public static Delegate Combine(Delegate[] delegateArray);

// Removes a delegate matching value�s Target/Method from the chain.
// The new head is returned and will be the last delegate called
public static Delegate Remove(Delegate source, Delegate value);
}

 

      When you construct a new delegate object, the object�s _prev field is set to null indicating that there are no other objects in the linked-list. To combine two delegates into a linked-list, you call one of Delegate�s static Combine methods:

  Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2);
// The left side of Figure 1 shows what the
// chain looks like after the previous code executes

App appobj = new App();
Feedback fb3 = new Feedback(appobj.FeedbackToStream);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);

 

Figure 1 Delegate Chains
Figure 1 Delegate Chains

      Figure 1 shows what the chain looks like after all the code executes. You�ll notice that the Delegate type offers another version of the Combine method that takes an array of Delegate references. Using this version of Combine, you could rewrite the code shown previously as follows:

  Feedback[] fbArray = new Feedback[3];
fbArray[0] = new Feedback(FeedbackToConsole);
fbArray[1] = new Feedback(FeedbackToMsgBox);
App appobj = new App();
fbArray[2] = new Feedback(appobj.FeedbackToStream);

Feedback fbChain = Delegate.Combine(fbArray);

 

      When a delegate is invoked, the compiler generates a call to the delegate type�s Invoke method (as I discussed in the previous column). To refresh your memory, the example in that column declared a Feedback delegate as the following code does:

  public delegate void     
Feedback(
Object value, Int32 item, Int32 numItems);

 

This caused the compiler to generate a Feedback class that contains an Invoke method that looks like this (in pseudocode):

  class Feedback : MulticastDelegate {
public void virtual Invoke(
Object value, Int32 item, Int32 numItems) {

// If there are any delegates in the chain that
// should be called first, call them
if (_prev != null) _prev.Invoke(value, item, numItems);

// Call our callback method on the specified target object
_target.methodPtr(value, item, numItems);
}
}

 

      As you can see, invoking a delegate object causes its previous delegate to be invoked first. When the previous delegate returns, its return value is discarded. After calling its previous delegate, the delegate can then invoke the callback target/method that it wraps. The code in Figure 2 demonstrates this process.
      So far, I�ve shown examples where my delegate type, Feedback, is defined with a void return value. If I had defined my Feedback delegate as follows

  public delegate Int32 Feedback(
Object value, Int32 item, Int32 numItems);

 

then its Invoke method would have internally looked like this (again, in pseudocode):

  class Feedback : MulticastDelegate {
public Int32 virtual Invoke(
Object value, Int32 item, Int32 numItems) {

// If there are any delegates in the chain that
// should be called first, call them
if (_prev != null) _prev.Invoke(value, item, numItems);

// Call our callback method on the specified target object
return _target.methodPtr(value, item, numItems);
}
}

 

      When the head of the delegate chain is invoked, it calls any previous delegate in the chain. Notice that the previous delegate�s return value is discarded. Your application code will only receive the return value from the delegate that is at the head of the chain (the last callback method called).
      Once constructed, delegate objects are considered to be immutable. That is, delegate objects always have their _prev fields set to null, and this never changes. When you combine a delegate object to a chain, Combine internally constructs a new delegate object that has the same _target and _methodPtr fields as the source object. The _prev field is set to the old head of the chain. The address of the new delegate object is returned from Combine (see the code in Figure 3).
      Now that you know how to build delegate chains, let�s see how to remove a delegate from a chain. To remove a delegate from a linked-list, you call the Delegate type�s static Remove method, as shown in Figure 4. The code first builds a chain by constructing two delegate objects, then combines them into a linked-list by calling the Combine method. Then the Remove method is called. Remove�s first parameter refers to the head of the delegate object chain and the second parameter refers to the delegate object that is to be removed from the chain. I know it seems strange to construct a new delegate object in order to remove it from the chain. To understand why this is necessary you�ll need some additional explanation.
      In the call to Remove, I�m constructing a new delegate object. This delegate object has its _target and _methodPtr fields initialized appropriately, and the _prev field is set to null. The Remove method scans the chain (referred to by fbChain), checking if any of the delegate objects in the chain are equal to the new delegate object. Remember, the overridden Equals method implemented by the Delegate class compares the _target and _methodPtr fields only and ignores the _prev field.
      If a match is found, then the Remove method removes the located delegate object from the chain by fixing up the previous delegate object�s _prev field. Remove returns the head of the new chain. If a match is not found, then Remove does nothing (no exception is thrown) and returns the same value that was passed for its first parameter.
      Each call to Remove eliminates only one object from the chain, as demonstrated by the code in Figure 5.
      To make things easier for C# developers, the C# compiler automatically provides overloads of the += and -= operators for instances of delegate types. These operators call Delegate.Combine and Delegate.Remove, respectively. Using these operators simplifies the building of delegate chains. The C# code in Figure 6 demonstrates how using the C# operators simplifies the code to combine and remove delegate objects from a chain.
      Internally, the compiler translates all uses of += on delegates to calls to Delegate�s Combine method. Likewise, all uses of the -= operator on delegate objects translate to calls to the Remove method. In fact, you can build the code I�ve just shown and look at its intermediate language using ILDasm.exe. This will confirm that the C# compiler did, in fact, replace all += and -= operators with calls to the Delegate type�s static Combine and Remove methods, respectively.

Invoking a Delegate Chain

      At this point, you understand how to build a linked-list chain of delegate objects and how to invoke all the objects in that chain. All items in the linked-list chain are invoked because the delegate type�s Invoke method includes code to call the previous delegate (if one exists). This is obviously a very simple algorithm. While it is good enough for many of the scenarios you may encounter, this logic has many limitations.
      For example, the return values of the callback methods are all discarded, except for the last. Using this simple algorithm, there is no way to get the return values for all the callback methods called. But this algorithm has even more limitations. For example, what happens if one of the invoked delegates throws an exception or blocks for a very long time? Since the algorithm invoked each delegate in the chain serially, a problem with one of the delegate objects stops all the other delegates in the chain from getting called. Clearly, this is not a robust algorithm.
      For those scenarios where this logic is insufficient, the MulticastDelegate class offers an instance method, GetInvocationList, that you can use to call each delegate in a chain explicitly, using any algorithm that meets your needs:

  public class MulticastDelegate {
// Creates a delegate array; each item is a clone from the chain
// (NOTE: entry 0 is the tail, which would normally be called first)
public virtual Delegate[] GetInvocationList();
}

 

      The GetInvocationList method operates on a reference to a delegate chain and returns an array of references to delegate objects. Internally, GetInvocationList walks the specified chain and creates a clone of each object in the chain, appending the clone to the array. Each clone has its _prev field set to null, so each object is isolated and does not refer to a chain of any other objects.
      Now you can easily write an algorithm that explicitly calls each object in the array. The code in Figure 7 shows such an algorithm.

Send questions and comments for Jeff to dot-net@microsoft.com.

Jeffrey Richter is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999), and is a cofounder of Wintellect (https://www.Wintellect.com), a software education, debugging, and consulting firm. He specializes in programming/design for .NET and Win32. Jeff is writing a Microsoft .NET Framework programming book and offers .NET technology seminars.