Lesson 16: Delegates and Events

 

Delegates are reference types which allow indirect calls to methods (lesson 13). A delegate instance holds references to some number of methods, and by invoking the delegate one causes all of these methods to be called. The usefulness of delegates lies in the fact that the functions which invoke them are blind to the underlying methods they thereby cause to run (see, for instance, the discussion of events, below).

From this brief description, it can be seen that delegates are functionally rather similar to C++'s 'function pointers'. However, it is important to bear in mind two main differences. Firstly, delegates are reference types rather than value types (for the difference see lesson 4). Secondly, some single delegates can reference multiple methods

Delegate Declaration and Instantiation

Delegates can be specified on their own in a namespace, or else can be specified within another class (the examples below all show the latter). In each case, the declaration specifies a new class, which inherits from System.MulticastDelegate.

Each delegate is limited to referencing methods of a particular kind only. The type is indicated by the delegate declaration - the input parameters and return type given in the delegate declaration must be shared by the methods its delegate instances reference. To illustrate this: a delegate specified as below can be used to refer only to methods which have a single String input and no return value:

public delegate void Print (String s);

Suppose, for instance, that a class contains the following method:

1.

public void realMethod (String myString)

2.

{

3.

    // method code

4.

}


Another method in this class could then instantiate the 'Print' delegate in the following way, so that it holds a reference to 'realMethod':

Print delegateVariable = new Print(realMethod);

We can note two important points about this example. Firstly, the unqualified method passed to the delegate constructor is implicitly recognised as a method of the instance passing it. That is, the code is equivalent to:

Print delegateVariable = new Print(this.realMethod);

We can, however, in the same way pass to the delegate constructor the methods of other class instances, or even static class methods. In the case of the former, the instance must exist at the time the method reference is passed. In the case of the latter (exemplified below), the class need never be instantiated.

Print delegateVariable = new Print(ExampleClass.exampleMethod);

The second thing to note about the example is that all delegates can be constructed in this fashion, to create a delegate instance which refers to a single method. However, as we noted before, some delegates - termed 'multicast delegates' - can simultaneously reference multiple methods. These delegates must - like our Print delegate - specify a 'void' return type.

One manipulates the references of multicast delegates by using addition and subtraction operators (although delegates are in fact immutable reference types - for explanation of the apparent contradiction see the discussion of strings in Lesson 4). The following code gives some examples:

1.

Print s = null;

2.

s = s + new Print (realMethod);

3.

s += new Print (otherRealMethod);


The - and -= operators are used in the same way to remove method references from a delegate.

The following code gives an example of the use of delegates. In the Main method, the Print delegate is instantiated twice, taking different methods. These Print delegates are then passed to the Display method, which by invoking the Print delegate causes the method it holds to run. As an exercise, you could try rewriting the code to make Print a multicast delegate.

1.

using System;

2.

using System.IO;

3.

4.

public class DelegateTest

5.

{

6.

    public delegate void Print (String s);

7.

8.

    public static void Main()

9.

    {

10.

        Print s = new Print (toConsole);

11.

        Print v = new Print (toFile);

12.

        Display (s);

13.

        Display (v);

14.

    }

15.

16.

    public static void toConsole (String str)

17.

    {

18.

        Console.WriteLine(str);

19.

    }

20.

21.

    public static void toFile (String s)

22.

    {

23.

        File f = new File("fred.txt");

24.

        StreamWriter fileOut = f.CreateText();

25.

        fileOut.WriteLine(s);

26.

        fileOut.Flush();

27.

        fileOut.Close();

28.

    }

29.

30.

    public static void Display(Print pMethod)

31.

    {

32.

        pMethod("This should be displayed in the console");

33.

    }

34.

}


Events

To recap: in object-oriented languages, objects expose encapsulated functions called methods. Methods are encapsulated functions which run when they are invoked.

Sometimes, however, we think of the process of method invocation more grandly. In such a case, the method invocation is termed an 'event', and the running of the method is the 'handling' of the event. An archetypal example of an event is a user's selection of a button on a graphical user interface; this action may trigger a number of methods to 'handle' it.

What distinguishes events from other method invocations is not, however, that they must be generated externally. Any internal change in the state of a program can be used as an event. Rather, what distinguishes events is that they are backed by a particular 'subscription-notification' model. An arbitrary class must be able to 'subscribe to' (or declare its interest in) a particular event, and then receive a 'notification' (ie. have one of its methods run) whenever the event occurs.

Delegates - in particular multicast delegates - are essential in realizing this subscription-notification model. The following example describes how Class 2 subscribes to an event issued by Class 1.

1. Class 1 is an issuer of E-events. It maintains a public multicast delegate D.
2. Class 2 wants to respond to E-events with its event-handling method M. It therefore adds onto D a reference to M.
3. When Class 1 wants to issue an E-event, it simply calls D. This invokes all of the methods which have subscribed to the event, including M.

The 'event' keyword is used to declare a particular multicast delegate (in fact, it is usual in the literature to just identify the event with this delegate). The code below shows a class EventIssuer, which maintains an event field myEvent. We could instead have declared the event to be a property instead of a field (for the difference between these see lesson 15). When the method issueEvent is called, the event myEvent is called, sending out both a reference to its parent object and arbitrary arguments.

1.

public class EventIssuer

2.

{

3.

    public delegate void EventDelegate(object from, EventArgs args);

4.

    public event EventDelegate myEvent;

5.

6.

    public void issueEvent(EventArgs args)

7.

    {

8.

        myEvent(this, args);

9.

    }

10.


A class which wanted to handle the events issued by an EventIssuer ei with its method handleEvents would then subscribe to these events with the code:

ei.myEvent += new EventIssuer.EventDelegate(handleEvents);

 

C# Tutorial