13

Delegates and Lambda Expressions

Previous chapters discussed extensively how to create classes to encapsulate data and operations on data. As you create more and more classes, you will see common patterns in the relationships among them. One common pattern is to pass an object to a method solely so that the method can, in turn, call a method on the object. For example, if you pass an IComparer<int> reference to a method, odds are good that the called method will itself call the Compare() method on the object you provided. In this case, the interface is nothing more than a way to pass a reference to a single method that can be invoked. Consider a second example in which you invoke a new process. Rather than blocking or repeatedly checking (polling) when the process has completed, ideally you would like to have the method run asynchronously and then invoke a callback function, which the called method will invoke to notify the caller when the asynchronous invocation completes.

It seems unnecessary to have to define a new interface every time you want to pass a method around. In this chapter, we describe how to create and use a special kind of class called a delegate that enables you to treat references to methods as you would any other data. We then show how to create custom delegate instances quickly and easily with lambda expressions.

This chapter includes Advanced Topic blocks that describe how to use anonymous methods should you need to work with legacy C# 2.0 code1; you can largely ignore these sections if you are working only with newer code.

We conclude the chapter with a discussion of expression trees, which enable you to use the compiler’s analysis of a lambda expression at execution time.

Introducing Delegates

Veteran C and C++ programmers have long used function pointers as a mechanism for passing a reference to one method as an argument to another method. C# achieves similar functionality by using delegates. Delegates allow you to capture a reference to a method and pass it around like any other object, and to call the captured method like any other method. Let’s consider an example illustrating how this technique might be useful.

Defining the Scenario

Although it is not very efficient, one of the simplest sort routines is the bubble sort. Listing 13.1 shows the BubbleSort() method.

Listing 13.1: BubbleSort() Method
public static class SimpleSort1
{
    public static void BubbleSort(int[] items)
    {
        int i;
        int j;
        int temp;
 
        if(items == null)
        {
            return;
        }
 
        for(i = items.Length - 1; i >= 0; i--)
        {
            for(j = 1; j <= i; j++)
            {
                if(items[j - 1] > items[j])
                {
                    temp = items[j - 1];
                    items[j - 1] = items[j];
                    items[j] = temp;
                }
            }
        }
    }
    // ...
}

This method will sort an array of integers in ascending order.

Suppose you need to sort the integers in Listing 13.1 in either ascending or descending order. You could duplicate the code and replace the greater-than operator with a less-than operator, but it seems like a bad idea to replicate several dozen lines of code merely to change a single operator. As a less verbose alternative, you could pass in an additional parameter indicating how to perform the sort, as shown in Listing 13.2.

Listing 13.2: BubbleSort() Method, Ascending or Descending
public class SimpleSort2
{
    public enum SortType
    {
        Ascending,
        Descending
    }
 
    public static void BubbleSort(int[] items, SortType sortOrder)
    {
        int i;
        int j;
        int temp;
 
        if(items == null)
        {
            return;
        }
 
        for(i = items.Length - 1; i >= 0; i--)
        {
            for(j = 1; j <= i; j++)
            {
 
                bool swap = false;
                switch(sortOrder)
                {
                    case SortType.Ascending:
                        swap = items[j - 1] > items[j];
                        break;
 
                    case SortType.Descending:
                        swap = items[j - 1] < items[j];
                        break;
                }
 
                if(swap)
                {
                    temp = items[j - 1];
                    items[j - 1] = items[j];
                    items[j] = temp;
                }
            }
        }
    }
    // ...
}

However, this code handles only two of the possible sort orders. If you want to sort them lexicographically (that is, 1, 10, 11, 12, 2, 20, …), or order them via some other criterion, it would not take long before the number of SortType values and the corresponding switch cases would become cumbersome.

Delegate Data Types

To increase flexibility and reduce code duplication in the previous code listings, you can make the comparison method a parameter to the BubbleSort() method. To pass a method as an argument, a data type is required to represent that method; this data type is generally called a delegate because it “delegates” the call to another method. You can use the name of a method as a delegate instance. You can also use a lambda expression as a delegate, to express a short piece of code “in place” rather than creating a method for it.2 Listing 13.3 includes a modification to the BubbleSort() method that takes a lambda expression parameter.3 Here, the delegate data type is Func<int, int, bool>.

Listing 13.3: BubbleSort() with Delegate Parameter
public class DelegateSample
{
    // ...
 
    public static void BubbleSort(
        int[] items, Func<intintbool> compare)
    {
        int i;
        int j;
        int temp;
 
        if(compare == null)
        {
            throw new ArgumentNullException(nameof(compare));
        }
 
        if(items == null)
        {
            return;
        }
 
        for(i = items.Length - 1; i >= 0; i--)
        {
            for(j = 1; j <= i; j++)
            {
                if (compare(items[j - 1], items[j]))
                {
                    temp = items[j - 1];
                    items[j - 1] = items[j];
                    items[j] = temp;
                }
            }
        }
    }
    // ...
}

The delegate of type Func<int, int, bool> represents a method that compares two integers. Within the BubbleSort() method, you then use the instance of the Func<int, int, bool>, referred to by the compare parameter, to determine which integer is greater. Since compare represents a method, the syntax to invoke the method is identical to calling any other method. In this case, the Func<int, int, bool> delegate takes two integer parameters and returns a Boolean value that indicates whether the first integer is greater than the second one:

if (compare(items[j - 1], items[j])) { ... }

Note that the Func<int, int, bool> delegate is strongly typed to represent a method that accepts exactly two integer parameters and returns a bool. Just as with any other method call, the call to a delegate is strongly typed, and if the data types for the arguments are not compatible with the parameters, the C# compiler reports an error.

________________________________________

1. Lambda expressions added in C# 3.0. Anonymous methods used in C# 2.0 and prior for custom delegate creation.
2. Starting in C# 3.0.
3. In C# 7.0, you can create a local function and then use the function name for the delegate as well.
{{ snackbarMessage }}