You just saw how to define a method that uses a delegate, and you learned how to invoke a call to the delegate simply by treating the delegate variable as a method. However, you have yet to learn how to declare a delegate type. To declare a delegate type, you use the delegate keyword and follow it with what looks like a method declaration. The signature of that method is the signature of the method that the delegate can refer to, and the name of the delegate type appears where the name of the method would appear in a method declaration. The Func<...> delegate in Listing 13.3, for example, is declared as4
public delegate TResult Func<in T1, in T2, out TResult>(
in T1 arg1, in T2 arg2)
Fortunately,5 it turns out you rarely, if ever, need to declare your own delegates. The need to define your own custom delegate types was effectively eliminated with the .NET 3.5 runtime library6 because it included a set of general-purpose delegates, most of them generic. The System.Func family of delegates is for referring to methods that return a value; the System.Action family of delegates is for referring to void-returning methods. The signatures for these delegates are shown in Listing 13.4.
Since the delegate definitions described in Listing 13.4 are generic, you can use them instead of defining your own custom delegate.
The first delegate type in Listing 13.4 is Action<...>. It is used to represent a method for which there is no return and supports methods with up to 16 parameters. For delegates that need to return a result, there is the Func<...> delegate. The last type parameter of Func<...> is TResult—the type of the return. The other type parameters on Func<...> correspond in sequence to the types of the delegate parameters. The BubbleSort method in Listing 13.3, for example, requires a delegate that returns bool and takes two int parameters.
The last delegate in Listing 13.4 is Predicate<in T>. When a lambda is used to return a bool, the lambda is called a predicate. However, this predicate is generally used to filter or identify items from a collection—you pass it an item and it returns true or false to indicate whether or not to filter the item. In contrast, our BubbleSort() example accepted two parameters for the purpose of comparing them, so Func<int, int, bool> was used instead of a predicate type.
In many cases, the inclusion of Func and Action delegates in Microsoft .NET Framework 3.5, and later in the .NET Standard, virtually eliminates the need to define your own delegate types. However, you should consider declaring your own delegate types when doing so significantly increases the readability of the code. A delegate named Comparer, for example, provides an explicit indication of what the delegate is used for, whereas using Func<int, int, bool> only identifies a delegate’s parameters and return type. Listing 13.5 shows how to declare the Comparer delegate type to require two integers and return a Boolean value.
Given the new delegate data type, you can update Listing 13.3 with a signature that replaces Func<int, int, bool> with Comparer:
public static void BubbleSort(int[] items, Comparer compare)
Just as classes can be nested in other classes, so delegates can also be nested in classes. If the delegate declaration appeared within another class, the delegate type would be a nested type, as shown in Listing 13.6.
In this case, the data type would be DelegateSample.ComparisonHandler because it is defined as a nested type within DelegateSample. Nesting should be considered when utilization is expected to be needed only from within the containing class.
In the final step of implementing the BubbleSort() method with a delegate, you will call the method and pass a delegate instance—specifically, an instance of type Func<int, int, bool>. To instantiate a delegate, you need a method with parameters and a return type that matches the signature of the delegate type itself. The name of the method need not match the name of the delegate, but the rest of the method signature must be compatible with the delegate signature. Listing 13.7 shows the code for a greater-than method compatible with the delegate type.
With this method defined, you can call BubbleSort() and supply as the argument the name of the method that is to be captured by the delegate, as shown in Listing 13.8.
Note that delegates are reference types, but you do not necessarily use new to instantiate them. The conversion from the method group—the expression that names the method—to the delegate type automatically creates a new delegate object.7
In Listing 13.8, the delegate was instantiated by simply passing the name of the desired method, GreaterThan, as an argument to the call to the BubbleSort() method. The first version of C# required instantiation of the delegate, using the more verbose syntax shown in Listing 13.9.
In this case, we use Comparer rather than Func<int, int, bool> because the latter wasn’t available in C# 1.0.
Later versions support both syntaxes; throughout the remainder of the book, we will show only the modern, concise syntax.
A delegate is actually a special kind of class. Although the C# standard does not specify exactly what the class hierarchy is, a delegate must always derive directly or indirectly from System.Delegate. In fact, in .NET, delegate types always derive from System.MulticastDelegate, which in turn derives from System.Delegate, as shown in Figure 13.1.
The first property is of type System.Reflection.MethodInfo. MethodInfo describes the signature of a method, including its name, parameters, and return type. In addition to MethodInfo, a delegate needs the instance of the object containing the method to invoke. This is the purpose of the second property, Target. In the case of a static method, Target is null. The purpose of the MulticastDelegate class is discussed in Chapter 14.
Note that all delegates are immutable; that is, you cannot change a delegate once you have created it. If you have a variable that contains a reference to a delegate and you want it to refer to a different method, you must create a new delegate and assign it to the variable.
Although all delegate data types derive indirectly from System.Delegate, the C# compiler does not allow you to declare a class that derives directly or indirectly from System.Delegate or System.MulticastDelegate. As a consequence, the code shown in Listing 13.10 is not valid.
Passing the delegate to specify the sort order is a significantly more flexible strategy than using the approach described at the beginning of this chapter. By passing a delegate, you can change the sort order to be alphabetical simply by adding an alternative delegate to convert integers to strings as part of the comparison. Listing 13.11 shows a full listing that demonstrates alphabetical sorting, and Output 13.1 shows the results.
The alphabetical order is different from the numeric order. Even so, notice how simple it was to add this additional sort mechanism compared to the process used at the beginning of the chapter. The only changes to create the alphabetical sort order were the addition of the AlphabeticalGreaterThan method and then passing that method into the call to BubbleSort().
________________________________________