Delegates in Visual J++ 6.0

Visual Studio 6.0
 

Microsoft Corporation

March 11, 1998

Introduction

Microsoft® Visual J++™ 6.0 introduces a powerful new language feature: delegates. Delegates enable scenarios that other languages—C++, Pascal, Modula, and others—have addressed with function pointers. Unlike function pointers, delegates are object-oriented, type-safe, and secure.

A delegate declaration defines a class that extends the class com.ms.lang.Delegate. A delegate instance encapsulates a method—a callable entity. For instance methods, a callable entity consists of an instance and a method on the instance. If you have a delegate instance and an appropriate set of arguments, you can invoke the delegate with the arguments.

An interesting and useful property of a delegate is that it does not know or care about the class of the object that it references. Any object will do; all that matters is that the method's signature matches the delegate's. This makes delegates perfectly suited for "anonymous" invocation. This is a powerful capability.

Overview of Delegates

A brief overview serves as a better introduction to this feature than a detailed language specification. There are three steps in defining and using delegates: declaration, instantiation, and invocation.

Declaring a Delegate

Delegates are declared using VJ++ delegate declaration syntax, which can appear in the same places in code as class declarations. When compiled, a delegate declaration results in a class that extends either com.ms.lang.Delegate or com.ms.lang.MulticastDelegate, depending on the presence or absence of the multicast keyword.

Here is the syntax:

DelegateDeclaration:
DelegateModifiersopt multicast opt delegate ResultType Identifier ( FormalParameterListopt ) Throwsopt 
DelegateModifiers: zero or more of 
public protected private static final 

As an example, consider the delegate declaration below, which takes two int parameters, returns a long, and raises no checked exceptions.

delegate long IntOp(int a, int b);

Instantiating a Delegate

VJ++ provides two ways to instantiate a delegate. The "late-bound" way is to use an (Object, String) delegate constructor. Use of this constructor requires a string lookup at runtime. To enable more efficient delegate instantiation, VJ++ provides language syntax for more efficient, "early-bound" delegate instantiation.

DelegateInstantiationExpression: 
new DelegateType ( MethodDesignator ) 
MethodDesignator: 
MethodName 
Primary . Identifier 
super . Identifier 
DelegateType: 
a class deriving from com.ms.lang.Delegate 

For example, the code below uses early-bound delegate instantiation to create an IntOp delegate that wraps the add method:

long add(int a, int b) 
{ 
return a+b; 
} 
void test() 
{ 
IntOp op = new IntOp(this.add); 
. . . 
} 

Invoking a Delegate

In addition to inheriting the Delegate or MulticastDelegate members, a delegate class also includes a member named "invoke" that can be used to call the method encapsulated by the delegate. The signature of this method is:

  ResultType invoke( FormalParameterListopt ) Throwsopt

For example:

long add(int a, int b) 
{ 
return a+b; 
} 
void test() 
{ 
IntOp op = new IntOp(this.add); 
long result = op.invoke(2, 3); 
} 

Multicast Delegates

A "regular" delegate encapsulates a reference to a method; when the delegate is invoked, the underlying method is called. VJ++ also provides support for "multicast" delegates; the effect of invoking a multicast delegate may be that multiple methods are invoked. The set of methods called by a delegate's invoke method is referred to as the delegate's "invocation list."

Multicast delegates must have a void result type—a single invocation can't return multiple results, so a result type wouldn't make much sense for multicast. When a multicast delegate is invoked, the methods in the invocation list are invoked synchronously, in the order in which they appear in the invocation list. If one of the methods raises an exception, then the multicast ceases, and the exception is propagated to the caller of the invoke method.

The base delegate classes make multicast support easy by providing invocation list management. There is a combine method that produces a new delegate that merges the invocation lists of two or more delegates, and a remove method that removes an item from an invocation list.

How Are Delegates Different from Function Pointers?

Delegates address many of the scenarios that are addressed by function pointers, though in a more modern manner. It would make no sense to add C++-style function pointers to the Java language. Doing so would violate the language's goals for object-orientation, type safety, and security. Let's take these goals one by one, and see how delegates differ from C++-style function pointers.

Delegates are object-oriented. Delegate declarations are compiled into classes, and delegate instances behave just like other instances. In contrast, C++ function pointers have no relationship at all with the object-oriented features of C++.

Delegates are type safe. The VJ++ compiler and the Microsoft Virtual Machine enforce type safety of delegates. To be encapsulated by a delegate, a method must precisely match the delegate in the following ways: number of arguments and types, return type, and exceptions thrown. In contrast, C++ function pointers may be cast arbitrarily. Incorrect casting can cause system crashes, and it is possible to treat an integer value as a function pointer, and vice versa. The same level of power (and danger) is not possible in VJ++.

Delegates are secure. The Microsoft Virtual Machine ensures that delegates interact well with the security system: Less trusted code can't create a delegate onto more trusted code and thus gain illicit access to additional capabilities. In contrast, C++-style function pointers do not work within a trust-based security system.

Example: Sorting

It is useful to examine a real-world example. Consider the problem of writing a sorting library that can sort an array of objects using a comparison method provided by the caller. In this example, we'll use delegates for the comparison method. The sorting package defines a delegate class. The caller implements a comparison method and instantiates a delegate that encapsulates it. When calling the sort routine, the caller passes an array (what to sort) and a comparison delegate (how to sort).

The first step in creating this sorting library is to define a delegate class that defines the comparison method's shape. We'll define the comparison method as taking two parameters of type Object, and returning an integer value. The comparison method returns the value 0 if the two objects are equivalent, the value 1 if (a > b) and the value –1 if (a < b). Here is an appropriate delegate, which we have named "Comparer":

delegate int Comparer(Object a, Object b);

When compiled, this declaration produces a class Comparer that extends Delegate. The Comparer class has an invoke method whose signature is:

int invoke(Object a, Object b); 

The next step is to define the sorting method. It takes a Comparer instance as a parameter, and uses this parameter to compare items, as shown below.

static void sort(Object[] items, Comparer c)
{
int i, pivot; 
. . . 
if (c.invoke(items[i], pivot) < 0) 
// less than pivot 
else 
// greater than or equal to pivot 
. . . 
}

To use the sorting package, a developer calls the sort method, passing a Comparer instance that encapsulates an appropriate comparison method. In the example shown below, the stringCompare method is employed.

void test() 
{ 
String[] stringArr = . . . 
Sorter.sort(stringArr, 
new Comparer(this.stringCompare)); 
} 
int stringCompare(Object a, Object b)
{ 
String x = (String) a;
String y = (String) b;
return x.compareTo(y); 
} 

A quick recap: there are three steps involved in defining and using a delegate:

  • Declaration
    delegate int Comparer(Object a, Object b);
    
    
  • Instantiation
    Comparer c = new Comparer(this.stringCompare); 
    
    
  • Invocation
    int comparisonValue = c.invoke(a, b); 
    
    

Delegates and Events

Though there are a broad set of uses for delegates, most developers will first encounter delegates when using the Windows Foundation Classes, a rich set of components that ease the development of Windows components and applications. WFC components use delegates to expose events. For example, WFC's Control base class exposes a number of mouse-oriented events: mouseDown, mouseEnter, mouseLeave, mouseMove, mouseUp, and mouseWheel.

The state for each of these events is packaged in a MouseEvent class. This class provides information about the event: which mouse buttons are depressed, whether the shift key is down, etc. The events also share in common a delegate class named MouseEventHandler. This delegate defines the shape of mouse event handlers.

multicast delegate void MouseEventHandler( Object sender, MouseEvent e); 

In WFC, components enable event handler hookup via addOnXxx and removeOnXxx methods, where Xxx is the name of the event. Here is the "add" and "remove" pair for the mouseMove event:

void addOnMouseMove(MouseEventHandler value); 
void removeOnMouseMove(MouseEventHandler value); 

The example below shows a form that has a mouseMove event handler for a button named "myButton":

class MyForm extends Form 
{ 
Button myButton = new Button(); 
void myButton_mouseMove(Object sender, 
MouseEvent e) 
{ 
p("mouse moved to " + e.x + "," + e.y); 
} 
void initForm() 
{ 
. . . 
myButton.addOnMouseMove(new MouseEventHandler(this.myButton_mouseMove)); 
. . . 
} 
} 

Delegates vs. Interfaces

Delegates and interfaces are similar in that they enable the separation of specification and implementation. Multiple independent authors can produce implementations that are compatible with an interface specification. Similarly, a delegate specifies the signature of a method, and authors can write methods that are compatible with the delegate specification. If VJ++ has interfaces already, why does it also need delegates?

Interfaces have a number of shortcomings that limit their usefulness:

  • A class can only implement an interface once.
  • Name collisions between interface members sometimes make it impossible for a class to implement multiple interfaces.
  • A class that implements an interface must expose the implemented members publicly.

Because of these shortcomings, it is frequently necessary to use "adapter classes." An adapter class is a surrogate used by a class that wants to implement an interface but can't do so, usually because of one of the limitations specified above. This shortcoming is particularly painful when using interfaces for eventing because it is quite common for a container to employ multiple instances of the same class. For example, forms commonly have multiple buttons, multiple text boxes and multiple labels. So with an interface-based event model, a typical form requires multiple adapter classes—approximately one per control instance. The result is a sea of adapter classes, whose collective size and cost easily overwhelm the size and cost of the form itself.

Delegates do not suffer the same problems as interfaces. Taking the points above individually:

  • A class can implement as many methods as it pleases, and create any number of delegate instances that wrap these members. This makes it possible to define all event handlers for a form on the form class itself; adapter classes are not needed.
  • There is no such thing as a "name collision" for a delegate. A method is compatible with a delegate if its signature and return type match the delegate's; the name of the method is immaterial.
  • Delegates can wrap private members, so it is possible to keep event handlers private—a highly desirable capability—without using adapter classes to hide them away.

Besides avoiding a plague of adapter classes, delegates have a number of other advantages over interfaces for event scenarios:

  • Delegates efficiently deal with the common case of unhandled events. The common case for an event is to be unhandled rather than handled. On the other hand, the common case for a component is to have at least one handled event. To maximize efficiency, an event system must avoid size and speed penalties for these unhandled events.

    Unfortunately, interface-based event systems have significant size and speed overhead for these unhandled cases. To handle one event, an event recipient must implement an entire event interface. Similarly, an event source must raise all of the events in an event interface, whether or not there are interesting event handlers on the other side. The former results in an unnecessary per-event size penalty; the latter results in an unnecessary per-event speed penalty.

    Delegate-based event systems sidestep this problem by employing fine granularity: Event handlers are connected one-by-one rather than en masse. Unhandled events have zero cost and handled events have a small cost.

  • Delegates make multicast support a breeze. Because delegates intrinsically support multicast, creating and managing multicast events is nearly trivial. The same task in interface-based event systems is difficult, time consuming and error-prone.
  • Delegates make event wiring easy. Delegates enable event wiring and forwarding scenarios that are difficult to support in interface-based systems.
Show: