A Java Programmer Looks at C# Delegates

来源:互联网 发布:vue组件引用js插件 编辑:程序博客网 时间:2024/05/17 03:43

A Java Programmer Looks at C# Delegates

http://www.onjava.com/pub/a/onjava/2003/05/21/delegates.html?page=1

Abstract

While C# has a set of capabilities similar to Java, it has added several new and interesting features. Delegation is the ability to treat a method as a first-class object. A C# delegate is used where Java developers would use an interface with a single method. In this article, the use of delegates in C# is discussed, and code is presented for a Java Delegate object that can perform a similar function. Download the source code here.

C#, pronounced C Sharp, is a language developed by Microsoft as a part of its .NET initiative. The language is extremely similar to Java. Were it not for legal difficulties between Microsoft and Sun, there is little question that Microsoft would have chosen Java to fill the role in its plans that is currently held by C#. The major features C# has in common with Java include garbage collection, a virtual machine, single inheritance, interfaces, packages (called namespaces), and the fact that all code is encapsulated within objects.

There are also a few significant differences between the two languages. Because the developers of C# had the advantage of carefully examining Java while developing their language, it is not surprising that some of the differences attempt to address significant problems that are difficult to deal with in Java. This article focuses on one item that Microsoft added to C#, why it was added, and how similar functionality could be implemented in Java.

Delegates in C#

C# introduces the concept of delegates, which represent methods that are callable without knowledge of the target object. Consider these situations:

Java Code

public class Class1 {    ...    public void show(String s) { .. }}public class Class2 {    ...    public void show(String s) { .. }}

Here, two classes share a common method, show, performing a similar function, display of data. We would like to be able to call that method in the same way for Class1 and Class2. If the two classes share a common interface, we can simply treat instances of either class as instances of that interface. Unfortunately, if the classes do not share an interface, as in the above example, there is no easy way to make a uniform call. If the developer controls the code for both classes, it is possible to retrofit a common interface. When one or both classes are in library code, there is no easy fix.

A more complex situation is illustrated in the example below:

Java Code

public class Class1 {    ...    public void show(String s) { .. }}public class Class2 {    ...    public void display(String s) { .. }}

These two classes have the methods show and display, which perform a similar function and have a similar signature. That is, they take similar arguments, return similar data, and could be used in a loop doing conceptually similar things. However, because the names of the two methods are different, no interface will recognize the two as performing the same action.

Java developers may address these issues through reflection (by generating an interface and implementing it with inner wrapper classes) or by constructing a dynamic proxy. All of these solutions are somewhat clumsy and require non-trivial amounts of code.

Consider how C# would address this issue. In the sample below, we define three classes that implement similar methods, two with different names and a third with a static implementing method:

C# Code

// define three classes with similar methods// two instances with differing names, one static.public class Class1 {    public void show(String s) { Console.WriteLine(s); }}public class Class2 {    public void display(String s) { Console.WriteLine(s); }}// allows static method as wellpublic class Class3 {    public static void staticDisplay(String s) { Console.WriteLine(s); }}

We will now define a new data type, doShow, which abstracts the common features of the methods in all three classes. That is, they all take a single string as an argument and return void. This is done using the C# delegatekeyword.

C# Code

public delegate void doShow(String s);

Think of a delegate as an interface declaring exactly one method. An instantiation of a delegate is similar to an anonymous inner class that implements the interface through a one-line call to a single method (static or instance) with a compatible signature.

With this new data type in hand, we may now arrange to invoke all three methods via a common abstraction:

C# Code

public class TestDelegate{    // define a datatype as a method taking a string returning void    public delegate void doShow(String s);    public static void Main(string[] args)     {        // make an array of these methods        doShow[] items = new doShow[3];        items[0] = new doShow(new Class1().show);        items[1] = new doShow(new Class2().display);        items[2] = new doShow(Class3.staticDisplay);        // call all items the same way        for(int i = 0; i < items.Length; i++) {            items[i]("Hello World");        }    }}

The main function creates an array populated with newly instantiated doShow objects. These refer to the various instance and class methods defined above. Let's take a closer look at the instantiation of a delegate:

C# Code

items[1] = new doShow(new Class2().display);

In C#, a method is a first-class object in the same way a Class likeString.class is a first-class object in Java. In C#, if we reference a method on an object (omitting the parentheses that would normally signal a method call), C# instead treats the method name like a field, returning the object representing that method. The constructor of a C# delegate expects to be called with just such a method object.

In Java terms, C# is dynamically creating an interface that declares a single method. When one considers how many interfaces (especially listeners and other event handlers) fit this description, the utility of this approach is apparent. C# uses delegates rather than Listener interfaces to implement most of its event handling. Threads in C# are constructed with delegates (System.Threading.ThreadStart), unlike Java, which uses the Runnable interface.

When an instance of a delegate is constructed, the actions the compiler takes are similar to the Java equivalent of building a wrapper class. This wrapper class exposes the interface defined by the declaration of the delegate, implementing the interface by calling the method that was passed to the delegate constructor.

The C# approach requires hooks into the compiler not available in Java. These hooks allow methods to be accessed at compile time via a convenient syntax in much the same way we would write String.classto access a known class at compile time. This approach allows the C# compiler to perform type checking on the arguments to a delegate invocation when compiling it, rather than throwing a type error at runtime

Implementing Delegates in Java

The code presented in this article implements a significant portion of the functionality of C# delegates in Java. Two ways of accomplishing this will be presented. In the first case, the developer describes the method to be delegated by providing a list of the parameter and return classes. In the second case, the parameters are deduced by examining a suitable interface that declares a single method. The code presents a factory class, Delegator, capable of handling either case. The factory method, build, returns an object implementing the Delegateinterface. Where the Delegator has been constructed with an interface, the return is a Proxy implementing that interface.



The Delegator Class

Two methods may be considered comparable if the argument lists of each method are assignable the same common list of classes and if the return types are assignable to a common type. As an example, if Foo is a superclass of Bar, the two methods

public String m1(Foo p1);public Object m2(Bar p1);

both match a signature taking Bar and returning Object. We may express this signature in code by providing aClass object that represents the return type and an array of Class to represent the parameter types. We may also express this signature by providing an interface with a single method to be used as an exemplar.

A method may match the signature described by a Delegator in the weak sense that all arguments are assignable to the declared types rather than the stronger test required by a Java interface that the arguments be identical. Also note that methods are considered comparable even it they throw different exceptions. Delegation will convert all exceptions into a runtime DelegateInvokeException emulating C# behavior (all C# exceptions act likeRuntimeExceptions).

Delegator provides a factory method, build, which associates the method template with a specific implementation. The implementation is a combination of either an instance method and a target instance or a static method. In either case, the method must be compatible with the requested signature. The object returned by thebuild method will satisfy the Delegate interface, which contains the method:

public Object invoke(Object[] args);

The returned object is a thin wrapper that invokes the method on the supplied object, converting any checked exceptions returned by the wrapped object into a runtime DelegateInvokeException. The code in Scenario 1 shows use of this basic type of Delegate object.

Scenario 1. Using a generic Delegate object

Java Code

class Class1 {    public void show(String s) { System.out.println(s); }}class Class2 {    public void display(String s) { System.out.println(s); }}// allows static method as wellclass Class3 {    public static void staticDisplay(String s) { System.out.println(s); }}public class TestDelegate  {    public static final Class[] OUTPUT_ARGS = { String.class };    public final Delegator DO_SHOW = new Delegator(OUTPUT_ARGS,Void.TYPE);    public void main(String[] args)  {        Delegate[] items = new Delegate[3];        items[0] = DO_SHOW .build(new Class1(),"show,);        items[1] = DO_SHOW.build (new Class2(),"display");        items[2] = DO_SHOW.build(Class3.class, "staticDisplay");        for(int i = 0; i < items.length; i++) {            items[i].invoke("Hello World");        }    }}

The code described above offers many of the advantages of C# delegates. Methods, either static or dynamic, can be treated in a uniform manner. The complexity in calling methods through reflection is reduced and the code is reusable, in the sense of requiring no additional classes in the user code. Note we are calling an alternate convenience version of invoke, where a method with one parameter can be called without creating an object array.

Scenario 2. Delegating via an interface

One advantage of C# delegates that is still missing is static type checking enforced by the compiler. In the example above, it is possible to call invoke on a returned Delegate with a Date object, and the error will not be discovered until run time. In order to get the full benefits of compiler support, it is necessary to construct the Delegator with an interface. The interface may be well-known or special-purpose, but should declare a single method. The signature of that method becomes the template used by the Delegator. In addition, the returned object will be a proxy that implements the requested interface.

Java Code

// interface to implementpublic static interface IStringDisplay {    public void doDisplay(String s);}public final Delegator ST_DEL  = new Delegator(IStringDisplay.class);public void testDelegate() {    IStringDisplay[] items = new IStringDisplay[3];    // build the delegates    items[0] = (IStringDisplay) ST_DEL.build(new Class1(),"show");    items[1] = (IStringDisplay) ST_DEL.build(new Class1()2,"display");    items[2] = (IStringDisplay) ST_DEL.build(Class3.class,"staticDisplay");    // call the delegates    for(int i = 0; i < items.length; i++) {        items[i].doDisplay("test");    }}

Note that while a cast is required to convert the value returned from the build method into an instance of the desired interface, invocations of the delegated method are now made through the interface with full static type checking.

Thread Delegates

One major use of delegates in C# is in threading. Rather than constructing a thread with an instance of Runnable, threads in C# are constructed by using a delegate, Thread.ThreadStart, with the appropriate signature. In Java, a similar problem exists where a developer wants to call a no-argument method as the active portion of aRunnable. While this may be accomplished with an anonymous inner class, the construct is clumsy and reduces the readability of the code.

This important usage pattern is supported by the Delegator class, which implements convenience methods to create delegates implementing Runnable. This is done by providing a static, final instance variable holding aDelegator constructed using the well-known interface Runnable, implementing two static methods that build delegates using this Delegator and cast the returned object to the underlying interface. Similar code could be used any time it is necessary to build many delegates that all implement a particular interface.

Java Code

static final Delegator RUNNABLE_DELEGATE = new Delegator(Runnable.class);public static Runnable buildRunnable(Object o,String methodName) {    return((Runnable)RUNNABLE_DELEGATE.build(o,methodName));}public static Runnable buildRunnable(Class c,String methodName) {    return((Runnable)RUNNABLE_DELEGATE.build(c,methodName));}

The above code can be called with a line such as:

Runnable r = Delegator. buildRunnable(this,methodName);

Note that because a special-purpose method has been used, there is no need to cast the return value frombuildRunnable. The cast is performed in the method implementation.

How It Works

The critical code is in the method build. This method constructs a DelegateProxy, exposed through anDelegate interface that is a wrapper around the method named in the call to build. If the Delegator was constructed by specifying an interface, the returned object is wrapped in a dynamic Proxy so that it will appear to the Java runtime as an instance of the requested interface.

Java Code

/**  * @param target non-null target with a bindable method  * @param MethodName  name of the  method  * @return non-null IDelegate. If getInterface() is non-null the returned   * Delegate will be a dynamic proxy implementing that interface  */ public Delegate build(Object target,String methodName){    Class myInterface         = getInterface();    DelegateProxy theDelegate = new DelegateProxy(target,methodName,this);    // build a dynamic proxy    if(myInterface != null) {        Class[] interfaces = { myInterface,Delegate.class };        Delegate ret       = (Delegate)java.lang.reflect.Proxy.newProxyInstance(                              target.getClass().getClassLoader(),                              interfaces, theDelegate);        return(ret);    }    return theDelegate;}

The constructor DelegateProxy(target,methodName,this) uses reflection to find the best method in the target class matching the signature contained in the Delegator. When an interface has been specified, theDelegateProxy can be used as an InvocationHandler to construct a Proxy implementing the specified interface. The returned object may then be called using the Delegate's invoke method or, if an interface is specified in the Delegator, cast to that interface and used as an instance of that interface.

Usage

Sufficient information to build delegates is present once classes are loaded. Binding a delegate is a non-trivial operation requiring identification of an appropriate method in the target class. When a delegate is constructed with an instance method, the build call can occur any time after the target instance has been created. Delegates invoking static methods can be constructed at load time. It is usually a good idea to build delegatees as early as possible, caching them for later use. Actually performing method calls through delegates is relatively cheap.


Timing and Performance Costs

The most logical use of delegates involves messaging and event handling where the code is infrequently executed; i.e., not in a tight loop. In these situations, executing the code contained in the target method usually takes significantly longer than the process of finding and invoking the method. In addition, Hotspot and JDK 1.4 have significantly reduced the cost of method calls. I found that executing the loop in TestDelegate (three calls) for 10,000,000 iterations took 43 seconds on a 1.5GHz Athlon processor running JDK 1.4 under Windows 2000. This averages slightly more than one microsecond per call. This cost can be ignored in all but the tightest loops.

Discussion

This approach represents an implementation of the Adapter pattern (Gamma et al, Design Patterns). It differs from a Proxy in that an Adapter maps a number of different methods into identical calls, allowing multiple objects implementing methods with different names but compatible signatures to be used interchangeably. It will also work with or without a target interface to implement. The Delegator class simplifies and generalizes the steps in creating an Adapter. Delegates are simple to use, and a single delegate instance may be reused multiple times.

Where an interface is known or can be constructed, use of delegates provides a simple, readable way to coerce a method into implementing the actions in that interface. Common interfaces, such as Runnable and many event listeners, may easily be mapped to any matching public method. In these cases, the Java code is almost as simple as the code that could be written using C#'s built-in delegate construct.

In my own development, I find that delegates implementing interfaces are more useful than those using invoke. The buildRunnable method is especially useful. In Swing programming, where large numbers of Runnables are needed to pass control to the swing thread, the ability to turn methods into Runnables is particularly useful. Delegates allow me to largely eliminate the need for anonymous inner classes, improving the readability of my code.

References

Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Addison Wesley Professional Computing Series, 1994.

Steven M. Lewis , PhD, is a Director of Development for UnifiedSignal, a provider of telecom solutions.

Wilhelm Fitzpatrick is an independent consultant specializing in Java development for enterprise platforms

0 0
原创粉丝点击