Multicast Delegate Internals

By Mark Michaelis (Mark Michaelis RSS)

In my last article, I outlined the internals of foreach and the way iteration works. I discussed the differences when iterating over arrays, as well as foreach’s interleaving and possible resource cleanup with a using clause.

In this article, I am going to switch to another fundament .NET language construct – delegates.   We will discuss the class structure of a delegate, specifically how multicast delegates use sequential notifications and, therefore, may require manually iterating over the delegate linked list rather than simply invoking the delegate. 

In this article, I assume readers are already familiar with the concepts around delegates – what they are used for as well as how to define and hook them up using C#.  Readers looking for information on delegates (either an introduction or a more advanced coverage) should check out the Delegates and Events chapter in my Essential C# 2.0 book (Addison-Wesley), where I start with the reason for delegates and move all the way to advanced coverage of the internals of how events work.

Delegates - Another Data Type

Defining a delegate is perhaps easier than defining a class.  Generally, in fact, it is just one or two lines of code (see Listing 1).

Listing 1: Declaring a delegate

public delegate bool TemperatureChangeHandler(
float newTemperature);

What is less obvious about the one or two lines of code, is that a delegate type definition is a shorthand syntax for defining a class (see Listing 2).

Listing 2: The C# comipler produces a class that derives from System.MulticastDelegate

// ERROR: 'GreaterThanHandler' cannot
// inherit from special class 'System.Delegate'
public class TemperatureChangeHandler: System.MulticastDelegate
{
...
}

Furthermore, System.MulticastDelegate derives from System.Delegate.  However, the delegate definition syntax is not just a short hand.  It turns out, the C# compiler prevents the declaration any class that derives (either directly or indirectly) from System.Delegate and therefore System.MulticastDelegate.

Each level within the delegate hierarchy provides a different set of services.  System.Delegate is a container of the data for what method to call on a particular object.  With System.MulticastDelegate comes the additional capability of not only invoking a method on a single object, but on a collections of objects.  This enables multiple subscribers to an event.

System.Delegate

The purpose of a single delegate instance is very similar to a method pointer from C++.   However, in C# we don’t use method pointers, rather, we save the “metadata” that identifies the target method to call.  System.Delegate contains two critical data elements.  Firstly, it contains an instance of System.Reflection.MethodInfo – in other words, the .NET metadata that enables method invocation using reflection.

The second aspect of System.Delegate is the object instance on which the method needs to be invoked.  Given an unlimited number of objects that could support a method that matches the MethodInfo signature, we also need to be able to identify which objects to notify.  The only exception is when the method identified by MethodInfo is static – in which case the object reference stored by System.Delegate is null.

System.MulticastDelegate

On its own, System.Delegate only identifies one subscriber to an event.  This would work for a method pointer, for example a predicate used in a sort routine, but it would rarely be sufficient in a publish-subscribe/observer pattern.  (In fact, in early betas of C# 1.0, Microsoft realized that using a single delegate was rare enough to avoid the need of even providing a means of defining a “single-cast” delegate.)

System.MulticastDelegate therefore, adds to delegates the support for notifying multiple subscribers.  This is enabled through System.MulticastDelegate’s containment of another System.MulticastDelegate instance.   When we add a subscriber to a multicast delegate, the MulticastDelegate class creates a new instance of the delegate type, stores the object reference and the method pointer for the added method into the new instance, and adds the new delegate instance as the next item in a list of delegate instances. In effect, the MulticastDelegate class maintains a linked list of delegate objects.

Sequential Invocation

When invoking the multicast delegate, each delegate instance in the linked list is called sequentially. (Generally, delegates are called in the order they were added but this behavior is not specified within the CLI specification and furthermore, it can be overridden. Therefore, programmers should not depend on an invocation order.)  This sequential invocation, however, leads to problems if the invoked method throws an exception or if the delegate itself returns data.

Error Handling Due to Sequential Invocation

Error handling makes awareness of the sequential notification critical. If one subscriber throws an exception then later subscribers in the chain do not receive the notification. Consider, for example, a heater class with an OnTemperatureChanged() method  that threw an exception as shown in Listing 3.

Listing 3:OnTemperatureChanged() Throwing an Exception

class Heater
{
  ...
  public voidOnTemperatureChanged( floatnewTemperature)
  {
       throw new NotImplementedException();
   }
  ...
}

Figure 1 shows the effect in a sequence diagram where cooler is an additional subscriber that also has a method matching the TemperatureChangeHandler() signature.

Figure 1: Delegate Invocation with Exception Sequence Diagram

Even though cooler subscribed to receive messages, heater exception terminates the chain and prevents the cooler object from receiving notification.

To avoid this problem, so that all subscribers receive notification regardless of the behavior of earlier subscribers, you must manually enumerate through the list of subscribers and call them individually. Listing 4shows an implementation for a   CurrentTemperature property that fires change notifications:

Listing 4: Handling Exceptions from Subscribers

public class Thermometer
{
  // Define the delegate data type
  public delegatevoid TemperatureChangeHandler(
     float newTemperature);
  // Define the event publisher
  public event TemperatureChangeHandler OnTemperatureChange;
  public float CurrentTemperature
  {
     get{return _CurrentTemperature;}
     set
      {
          if (value != CurrentTemperature)
          {
              _CurrentTemperature = value;
              if(OnTemperatureChange != null)
              {
                 foreach(
                      TemperatureChangeHandler handler in
                      OnTemperatureChange.GetInvocationList() )
                  {
                     try
                      {
                          handler(value);
                      }
                     catch(Exception exception)
                      {
                          Console.WriteLine(exception.Message);
                      }
                  }
                }
          }
      }
  }
  private float _CurrentTemperature;
}

Output:

Enter temperature: 45

The method or operation is not implemented.

Cooler: Off

This listing demonstrates that we can retrieve a list of subscribers from a delegates GetInvocationList() method. Enumerating over each item in this list returns the individual subscribers. If we then place each invocation of a subscriber within a try catch block we can handle any error conditions before continuing on with the enumeration loop. In our sample, even though heater.OnTemperatureChanged() throws an exception, cooler still receives the notification of the temperature change.

Method Returns and Pass-By-Reference

There is another scenario where it is useful to iterate over the delegate invocation list rather than simply activating a notification directly. This scenario relates to delegates that don't return void or else have ref or out parameters. In the thermometer example above, the OnTemperatureHandler delegate had a return type of void. Furthermore, we did not include any parameters that were ref or out type parameters, parameters that return data back to the caller. This is important because an invocation of a delegate potentially triggers notification to multiple subscribers. If the subscribers return a value, it is ambiguous which subscriber’s return value would be used.

If we changed OnTemperatureHandler to return an enumeration value, indicating whether the device was on because of the temperature change, the new delegate would be as shown in Listing 5.

Listing 5: Declaring a Delegate with a Method Return

public enum Status
{
    On,
    Off
}
// Define the delegate data type
public delegate Status TemperatureChangeHandler(
float newTemperature);

All subscriber methods would have to use the same method signature as the delegate, and therefore, each would be required to return a status value. Assuming we invoke the delegate in a similar manner as before, what will the value of status be in Listing 6, for example?

Listing 6: Invoking a Delegate Instance with a Return

Status status = OnTemperatureChange(value);

Since OnTemperatureChange potentially corresponds to a chain of delegates, status reflects only the value of the last delegate. All other values are lost entirely.

To overcome this issue, it is necessary to follow the same pattern that you used for error handling. In other words, you must iterate through each delegate invocation list, using the GetInvocationList() method, to retrieve each individual return value. Similarly, delegates types that use ref and out parameters need special consideration.

Conclusion

By no means is iteration over a delegate linked list always necessary, however, developers should be aware of how the delegate works because in many situations it is critical that the entire delegate linked list is called and a lack of understanding of the internals may cause a potentially critical bug to be released to the field.  In my classes on C#, I give the fictitious example of multiple subscribers to a lottery in which you have to be present to win.  The first subscriber throws an exception if (s)he is not the winner.  This prevents notification of other subscribers and eventually enables the first subscriber to win.