2.12. The delegate Type

A delegate type creates a kind of function object. The delegate type looks like a function declaration, but instead it defines a type that can refer to one or multiple functions of a particular signature and return type. A delegate type has three primary characteristics:

  1. A delegate object can address multiple methods rather than only one method at a time. When we invoke a delegate that addresses multiple methods, the methods are invoked in the order in which they are assigned to the delegate object. We'll see how to do that shortly.

  2. The methods addressed by a delegate object do not need to be members of the same class. All the methods addressed by a delegate object must share the same prototype and signature. Those methods, however, can be a combination of both static and nonstatic methods, and they may be members of one or more different classes.

  3. A declaration of a delegate type internally creates a new subtype instance of either the Delegate or the MulticastDelegate abstract base class of the .NET library framework, supporting a collection of public methods to query the delegate object and the method(s) to which it refers.

The declaration of a delegate type generally consists of four components: (1) an access level, (2) the keyword delegate, (3) the return type and signature of the method that the delegate type addresses, and (4) the name of the delegate type, which is placed between the return type and the signature of the method.

The following, for example, declares Action to be a public delegate type that addresses methods taking no parameters and with a return type of void:

public delegate void Action();

If a delegate type is used to address only a single method at any one time, it may address a member function of any return type and signature. If, however, the delegate type addresses two or more methods simultaneously, the return type must be void. Action, for example, can be used to address either a single or multiple methods.

For example, consider the design of a testHarness class. It must permit any class to register one or more either static or nonstatic class methods for subsequent execution. The delegate type is at the center of this implementation.

Let's declare the delegate object as a private static member of our testHarness class—for example,

public delegate void Action();
public class testHarness
{
    static private Action theAction;
    static public Action Tester
    {
           get{ return theAction;  }
           set{ theAction = value; }
    }

    // ...
}

A delegate type in C# is a reference type. Therefore its declaration—for example,

Action theAction;

represents a handle to a delegate object of the Action delegate type, but is not itself a delegate object. By default this handle is set to null. If we attempt to use it before it is assigned a value, a compile-time error is generated. For example, the statement

theAction();

causes invocation of the method(s) addressed by theAction. However, unless it has been unconditionally assigned to between being defined and this use, the invocation triggers a compile-time error message.

To set theAction to address a class member function, we must create an Action delegate type using the new expression. For a static method, the argument to the constructor is the name of the class to which the method belongs and the name of the method itself, joined by the dot operator (.):

theAction = new Action( Announce.announceDate );

For a nonstatic method, the argument to the constructor is the class object through which we wish to invoke the method joined to the method name—again joined by the dot operator:

Announce an = new Announce();
theAction   = new Action( an.announceTime );

The static announceDate() member function of the Announce class prints the current date to standard output in the long form,

Monday, February 26, 2001

while the nonstatic announceTime() member function prints the current time to standard output in the short form, 00:58, where the first two digits represent the hour, beginning at 00 for midnight, and the second two digits represent the minute. The class definition makes use of the DateTime class provided within the .NET class framework. We look at it in more detail in Section 5.5.4.

public class Announce
{
    public static void announceDate()
    {
          DateTime dt = DateTime.Now;
          Console.WriteLine( "Today's date is {0}",
                              dt.ToLongDateString() );
    }

    public void announceTime()
    {
        DateTime dt = DateTime.Now;
        Console.WriteLine( "The current time is {0}",
                              dt.ToShortTimeString() );
    }
}

As we saw earlier, we invoke the method addressed by the delegate object by applying the call operator to the delegate

testHarness.Tester();

First the get accessor of the Tester property is invoked. This returns the theAction delegate handle. The call operator (()) is then applied to the handle, resulting in the invocation of the method addressed by the delegate object. However, if theAction is not currently addressing a delegate object when the call operator is applied, an exception is thrown. To safeguard against this, we use the canonical delegate-test-and-execute sequence. From outside the class, this sequence looks like this:

					if ( testHarness.Tester != null )
     testHarness.Tester();

From within the class, it looks like this:

static public void run()
{
   if ( theAction != null )
         Action();
}

To assign a delegate address more than a single method, we use primarily the += and -= operators. For example, imagine that we have defined a testHashtable class. Within its static constructor, we add each associated test to the testHarness object:

public class testHashtable
{
    public void test0();
    public void test1();
    static testHashtable()
    {
        testHarness.Tester += new testHarness.Action( test0 );
        testHarness.Tester += new testHarness.Action( test1 );
    }
    // ...
}

Similarly, when we define a testArrayList class, we add each associated test within its static constructor. Notice that these methods are static:

public class testArrayList
{
    static public void testCapacity();
    static public void testSearch();
    static public void testSort();

    static testArrayList()
 {
      testHarness.Tester += new testHarness.Action(testCapacity);
      testHarness.Tester += new testHarness.Action(testSearch);
      testHarness.Tester += new testHarness.Action(testSort);
    }
    // ...
}

What is the order of invocation? The multiple methods are invoked in the order in which they are added to the delegate. So, for example, test0 is always invoked before test1 for the testHashtable class. Because we cannot know in general when a static constructor is invoked—only that it is invoked prior to a use of the class—we cannot say with certainty whether the testArrayList member functions or the testHashtable member functions are added to Tester first.

Consider the following code sequence within a local block:

{
    Announce annc = new Announce();
    testHarness.Tester +=
            new testHarness.Action( annc.announceTime );
}

When we initialize a delegate object to a nonstatic method, both the address of the method and a handle to the class object through which to invoke the method are stored. The result is that the class object's associated reference count is incremented.

When annc is initialized with the new expression, the associated reference count of the object on the managed heap is initialized to 1. When annc is passed to the constructor of the delegate object, the Announce object's reference count is incremented to 2. With the termination of the local block, the lifetime of annc terminates, and the reference count is decremented back to 1.

The good news is that the object associated with an invocation of a method referred to by a delegate object is guaranteed not to be garbage-collected until the delegate object no longer references the method. We don't have to worry about the object being cleaned up out from under us. The bad news is that the object persists until the delegate object no longer references the method.

The method can be removed with the -= operator. For example, the following revised local block first sets, then executes, and then removes announceTime() from the delegate object:

{
    Announce an = new Announce();
    Action act  = new testHarness.Action( an.announceTime );
    testHarness.Tester += act;
    testHarness.run();
    testHarness.Tester -= act;
}

An alternative implementation might first check if Tester already addresses one or several other methods. If so, it would save the currently set delegation list, reset Tester to act, invoke run(), and then reset Tester to the original delegation list.

To discover the number of methods a delegate addresses, we can make use of the underlying Delegate class interface—for example,

if ( testHarness.Tester != null &&
     testHarness.Tester.GetInvocationList().Length != 0 )
{
    Action oldAct = testHarness.Tester;

    testHarness.Tester = act;
    testHarness.run();
    testHarness.Tester = oldAct;
}
else { ... }

GetInvocationList() returns an array of Delegate class objects, each element of which represents a method currently addressed by the delegate object. Length is a property of the underlying Array class that implements the built-in C# array type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.104.183