Declaring Delegate Types

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)

General-Purpose Delegate Types: System.Func and System.Action

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.

Listing 13.4: Func and Action Delegate Declarations
1. public delegate void Action();
2. public delegate void Action<in T>(T arg);
3. public delegate void Action<in T1, in T2>(
4.     T1 arg1, T2 arg2);
5. public delegate void Action<in T1, in T2, in T3>(
6.     T1 arg1, T2 arg2, T3 arg3);
7. public delegate void Action<in T1, in T2, in T3, in T4>(
8.     T1 arg1, T2 arg2, T3 arg3, T4 arg4);
9.  
10.  ...
11.  
12. public delegate void Action<
13.     in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8,
14.     in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(
15.         T1 arg1, T2 arg2, T3 arg3, T4 arg4,
16.         T5 arg5, T6 arg6, T7 arg7, T8 arg8,
17.         T9 arg9, T10 arg10, T11 arg11, T12 arg12,
18.         T13 arg13, T14 arg14, T15 arg15, T16 arg16);
19.  
20. public delegate TResult Func<out TResult>();
21. public delegate TResult Func<in T, out TResult>(T arg);
22. public delegate TResult Func<in T1, in T2, out TResult>(
23.     T1 arg1, T2 arg2);
24. public delegate TResult Func<in T1, in T2, in T3, out TResult>(
25.     T1 arg1, T2 arg2, T3 arg3);
26. public delegate TResult Func<in T1, in T2, in T3, in T4,
27.     out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
28.  
29.  ...
30.  
31. public delegate TResult Func<
32.     in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8,
33.     in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16,
34.     out TResult>(
35.         T1 arg1, T2 arg2, T3 arg3, T4 arg4,
36.         T5 arg5, T6 arg6, T7 arg7, T8 arg8,
37.         T9 arg9, T10 arg10, T11 arg11, T12 arg12,
38.         T13 arg13, T14 arg14, T15 arg15, T16 arg16);
39.  
40. public delegate bool Predicate<in T>( T obj)

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.

Guidelines
CONSIDER whether the readability benefit of defining your own delegate type outweighs the convenience of using a predefined generic delegate type.
AdVanced Topic
Declaring a Delegate 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.

Listing 13.5: Declaring a Delegate Type
1. public delegate bool Comparer(
2.     int first, int second);

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.

Listing 13.6: Declaring a Nested Delegate Type
1. public class DelegateSample
2. {
3.     public delegate bool ComparisonHandler(
4.         int first, int second);
5. }

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.

Instantiating a Delegate

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.

Listing 13.7: Declaring a Func<int, int, bool>-Compatible Method
1. public class DelegateSample
2. {
3.     public static void BubbleSort(
4.         int[] items, Func<intintbool> compare)
5.     // ...
6.     public static bool GreaterThan(int first, int second)
7.     {
8.         return first > second;
9.     }
10.  
11.     // ...
12. }

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.

Listing 13.8: Using a Method Name as an Argument
1. public class DelegateSample
2. {
3.     public static void BubbleSort(
4.         int[] items, Func<intintbool> compare)
5.     // ...
6.     public static bool GreaterThan(int first, int second)
7.     {
8.         return first > second;
9.     }
10.  
11.     public static void Main()
12.     {
13.         int[] items = new int[5];
14.  
15.         for(int i = 0; i < items.Length; i++)
16.         {
17.             Console.Write("Enter an integer: ");
18.             string? text = Console.ReadLine();
19.             if (!int.TryParse(text, out items[i]))
20.             {
21.                 Console.WriteLine($"'{text}' is not a valid integer.");
22.                 return;
23.             }
24.         }
25.  
26.         BubbleSort(items, GreaterThan);
27.  
28.         for (int i = 0; i < items.Length; i++)
29.         {
30.             Console.WriteLine(items[i]);
31.         }
32.     }
33. }

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

AdVanced Topic
Delegate Instantiation in C# 1.0

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.

Listing 13.9: Passing a Delegate as a Parameter in C# 1.0
1. BubbleSort(items,
2.     new Comparer(GreaterThan));

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.

AdVanced Topic
Delegate Internals

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.

Figure 13.1: Delegate types object model

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.

Listing 13.10: System.Delegate Cannot Explicitly Be a Base Class
1. // ERROR: Func<T1, T2, TResult> cannot 
2. // inherit from special class 'System.Delegate'
3. public class Func<T1, T2, TResult>: System.Delegate
4. {
5. //    ...
6. }

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.

Listing 13.11: Using a Different Func<int, int, bool> Compatible Method
1. using System;
2.  
3. public class DelegateSample
4. {
5.     public static void BubbleSort(
6.         int[] items, Func<intintbool> compare)
7.     {
8.         int i;
9.         int j;
10.         int temp;
11.  
12.         for(i = items.Length - 1; i >= 0; i--)
13.         {
14.             for(j = 1; j <= i; j++)
15.             {
16.                 if(compare(items[j - 1], items[j]))
17.                 {
18.                     temp = items[j - 1];
19.                     items[j - 1] = items[j];
20.                     items[j] = temp;
21.                 }
22.             }
23.         }
24.     }
25.  
26.     public static bool GreaterThan(int first, int second)
27.     {
28.         return first > second;
29.     }
30.  
31.     public static bool AlphabeticalGreaterThan(
32.         int first, int second)
33.     {
34.         int comparison;
35.         comparison = (first.ToString().CompareTo(
36.             second.ToString()));
37.  
38.         return comparison > 0;
39.     }
40.  
41.     public static void Main()
42.     {
43.         int[] items = new int[5];
44.  
45.         for(int i = 0; i < items.Length; i++)
46.         {
47.             Console.Write("Enter an integer: ");
48.             string? text = Console.ReadLine();
49.             if (!int.TryParse(text, out items[i]))
50.             {
51.                 Console.WriteLine($"'{text}' is not a valid integer.");
52.                 return;
53.             }
54.         }
55.  
56.         BubbleSort(items, AlphabeticalGreaterThan);
57.  
58.         for (int i = 0; i < items.Length; i++)
59.         {
60.             Console.WriteLine(items[i]);
61.         }
62.     }
63. }
Output 13.1
Enter an integer: 1
Enter an integer: 12
Enter an integer: 13
Enter an integer: 5
Enter an integer: 4
1
12
13
4
5

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().

________________________________________

4. in/out type modifiers were not added until C# 4.0.
5. Starting with C# 3.0.
6. Corresponds to C# 3.0.
7. Starting in C# 2.0.
{{ snackbarMessage }}
;