Anonymous Methods
Up to this point, a method must already exist
in order for the delegate to work (that is, the delegate is defined
with the same signature as the method(s) it will be used with).
However, there is another way to use delegates - with anonymous
methods. An anonymous method is a block of code that is used as the
parameter for the delegate.
The syntax for defining a delegate with an
anonymous method doesn’t change. It’s when the delegate is
instantiated that things change. The following is a very simple
console app that shows how using an anonymous method can work:
The delegate DelegateTest is defined inside the class
Program. It takes a single string
parameter. Where things become different is in the Main method. When anonDel
is defined, instead of passing in a known method name, a simple
block of code is used prefixed by the delegate keyword followed by
a parameter:
As you can see, the block of code uses a
method-level string variable, mid, which
is defined outside of the anonymous method and adds it to the
parameter that was passed in. The code then returns the string
value. When the delegate is called, a string is passed in as the
parameter and the returned string is output to the console.
The benefit of anonymous methods is to reduce the
code you have to write. You don’t have to define a method just to
use it with a delegate. This becomes very evident when defining the
delegate for an event. (Events are discussed later in this
chapter.) This can help reduce the complexity of code, especially
where there are several events defined. With anonymous methods, the
code does not perform faster. The compiler still defines a method;
the method just has an automatically assigned name that you don’t
need to know.
A couple of rules must be followed when using
anonymous methods. You can’t have a jump statement (break, goto, or
continue) in an anonymous method that
has a target outside of the anonymous method. The reverse is also
true - a jump statement outside the anonymous method cannot have a
target inside the anonymous method.
Unsafe code cannot be accessed inside an anonymous
method. Also, ref and out parameters that are used outside of the
anonymous method cannot be accessed. Other variables defined
outside of the anonymous method can be used.
If you have to write the same functionality more
than once with anonymous methods, don’t use anonymous methods in
that case. Here writing a named method is the preferred way that
you only have to write once and reference it by its name.
SimpleDelegate Example
This example defines a MathsOperations class that has a couple of static
methods to perform two operations on doubles. Then you use
delegates to call up these methods. The math class looks like
this:
You call up these methods like this:
In this code, you instantiate an array of
DoubleOp delegates (remember that once
you have defined a delegate class, you can basically instantiate
instances just like you can with normal classes, so putting some
into an array is no problem). Each element of the array gets
initialized to refer to a different operation implemented by the
MathOperations class. Then, you loop
through the array, applying each operation to three different
values. This illustrates one way of using delegates - that you can
group methods together into an array using them, so that you can
call several methods in a loop.
The key lines in this code are the ones in which
you actually pass each delegate to the ProcessAndDisplayNumber() method, for example:
Here, you are passing in the name of a delegate but
without any parameters. Given that operations[i] is a delegate, syntactically:
-
operations[i]
means the delegate (that is, the method
represented by the delegate).
-
operations[i](2.0)
means actually call this method, passing in the
value in parentheses.
The ProcessAndDisplayNumber() method is defined to take
a delegate as its first parameter:
Then, when in this method, you call:
This actually causes the method that is wrapped up
by the action delegate instance to be
called and its return result stored in Result.
Running this example gives you the following:
If anonymous methods were used in this example, the
first class, MathOperations, could be
completely eliminated. The Main method
would then look like this:
Running this version will give you the same results
as the previous example. The advantage is that it eliminated a
class.
BubbleSorter Example
You are now ready for an example that will
show delegates working in a situation in which they are very
useful. You are going to write a class called BubbleSorter. This class implements a static method,
Sort(), which takes as its first
parameter an array of objects, and rearranges this array into
ascending order. For example, if you were to pass it this array of
ints: {0,5,6,2,1}, it would rearrange this array into
{0, 1, 2, 5, 6}.
The bubble-sorting algorithm is a well-known and
very simple way of sorting numbers. It is best suited to small sets
of numbers, because for larger sets of numbers (more than about 10)
far more efficient algorithms are available). It works by
repeatedly looping through the array, comparing each pair of
numbers and, if necessary, swapping them, so that the largest
numbers progressively move to the end of the array. For sorting
ints, a method to do a bubble sort might
look like this:
This is all very well for ints, but you want your Sort() method to be able to sort any object. In
other words, if some client code hands you an array of Currency structs or any other class or struct that
it may have defined, you need to be able to sort the array. This
presents a problem with the line if(sortArray[j] <
sortArray[i]) in the preceding code, because that requires
you to compare two objects on the array to see which one is
greater. You can do that for ints, but
how are you to do it for some new class that is unknown or
undecided until runtime? The answer is the client code that knows
about the class will have to pass in a delegate wrapping a method
that will do the comparison.
You define the delegate like this:
And you give your Sort
method this signature:
The documentation for this method states that
gtMethod must refer to a static method
that takes two arguments, and returns true if the value of the second argument is
greater than (that is, should come later in
the array than) the first one.
|
|
Tip |
Although you are using delegates here, it is
possible to solve this problem alternatively by using interfaces.
.NET in fact makes the IComparer
interface available for that purpose. However, delegates are used
here because this is still the kind of problem that lends itself to
delegates.
|
Now you are all set. Here is the definition for the
BubbleSorter class:
To use this class, you need to define some other
class, which you can use to set up an array that needs sorting. For
this example, assume that the Mortimer Phones mobile phone company
has a list of employees and wants them sorted according to salary.
The employees are each represented by an instance of a class,
Employee, which looks like this:
Notice that in order to match the signature of the
CompareOp delegate, you had to define
RhsIsGreater in this class as taking two
object references, rather than Employee
references as parameters. This means that you had to cast the
parameters into Employee references in
order to perform the comparison.
Now you are ready to write some client code to
request a sort:
Running this code shows that the Employees are correctly sorted according to
salary:
Multicast Delegates
So far, each of the delegates you have used
wraps just one single method call. Calling the delegate amounts to
calling that method. If you want to call more than one method, you
need to make an explicit call through a delegate more than once.
However, it is possible for a delegate to wrap more than one
method. Such a delegate is known as a multicast
delegate. If a multicast delegate is called, it will
successively call each method in order. For this to make sense, the
delegate signature should return a void;otherwise, you would only get the result of the
last method that is invoked by the delegate.
Consider the following code, which is adapted from
the SimpleDelegate example. Although the
syntax is the same as before, it is actually a multicast delegate,
Operations, that gets instantiated:
Using delegate inference you can write the previous
code. Also, this way that may be even easier to understand:
In the earlier example, you wanted to store
references to two methods, so you instantiated an array of
delegates. Here, you simply add both operations into the same
multicast delegate. Multicast delegates recognize the operators
+ and +=.
Alternatively, you can also expand the last two lines of the
preceding code as in this snippet:
Multicast delegates also recognize the operators
-- and -= to
remove method calls from the delegate.
|
|
Tip |
In terms of what’s going on under the hood, a
multicast delegate is a class derived from System.MulticastDelegate, which in turn is derived
from System.Delegate. System. MulticastDelegate
has additional members to allow chaining of method calls together
into a list.
|
To illustrate the use of multicast delegates, the
following code recasts the SimpleDelegate example into a new example,
MulticastDelegate. Because you now need
the delegate to refer to methods that return void, you have to rewrite the methods in the
MathOperations class, so they display
their results instead of returning them:
To accommodate this change, you also have to
rewrite ProcessAndDisplayNumber:
Now you can try out your multicast delegate like
this:
Now, each time ProcessAndDisplayNumber is called, it will display a
message to say that it has been called. Then the following
statement will cause each of the method calls in the action delegate instance to be called in
succession:
Running this code produces this result:
If you are using multicast delegates, you should be
aware that the order in which methods chained to the same delegate
will be called is formally undefined. You should, therefore, avoid
writing code that relies on such methods being called in any
particular order.
There might be even a bigger problem by invoking
multiple methods by one delegate. The multicast delegate contains a
collection of delegates to invoke one after the other. If one of
the methods invoked by a delegate throws an exception, the complete
iteration stops. Let’s have a look at the following MulticastIteration example. Here a simple delegate
that returns void without arguments named DemoDelegate is defined. This delegate is meant to
invoke the methods One() and
Two() that fulfill the parameter and
return type requirements of the delegate. Be aware that method
One() throws an exception.
In the Main() method
delegate d1 is created to reference
method One(), next the address of method
Two() is added to the same delegate.
d1 is invoked to call both methods. The
exception is caught in a try/catch block.
Only the first method is invoked by the delegate.
Because the first method throws an exception, iterating the
delegates stops here and method Two() is
never invoked. The result might differ as the order of calling the
methods is not defined.
In such a scenario, you can avoid the problem by
iterating the list on your own. The Delegate class defines the method GetInvocationList() that returns an array of
Delegate objects. You can now use this
delegate to invoke the methods associated with them directly, catch
exceptions and continue with the next iteration.
When you run the application with the code changes,
you can see that the iteration continues with the next method after
the exception is caught.