C# - Delegates and lambdas 101 [Beginner]

 

If you search for definitions of the word "delegate", you might find phrases such as “transferring power to someone” or “assign a task to a person”. Those phrases describe very well what delegates in .NET are about. In short they provide an ability to pass a reference to a method to some code that can then invoke that method.

The purpose of this article is to demystify delegates and help you gain some understanding about how different ways of using delegates relate to each other.

Let’s start with a simple sample of delegate usage:
class Program
{
    static void Main(string[] args)
    {
        PerformMathOperation(Add);
        PerformMathOperation(Subtract);
    }

    public static void PerformMathOperation(Func<int, int, int> func)
    {
        int result = func(5, 4);
        Console.WriteLine(result);
    }

    public static int Add(int x, int y)
    {
        return x + y;
    }

    public static int Subtract(int x, int y)
    {
        return x - y;
    }
}
 
The most interesting method in this code sample is PerformMathOperation. It takes one parameter of the type Func<int, int, int>. This is a generic delegate that takes two int values as parameters, and that returns an int value. This means that this delegate can represent any method fulfilling that signature. Note how the name of the method is completely irrelevant from the delegate perspective. All that matters is the signature, and the signature is the combination of the type of the return value, the number of parameters and their types.

In the code sample above, I have left out the creation of the actual delegate object; this is taken care of automatically by the C# compiler. The statement PerformMathOperation(Add) is identical to PerformMathOperation(new Func<int, int, int>(Add)), only shorter and – in my opinion – easier to read.

In the main method, I call PerformMathOperation twice. The first time I pass a delegate referencing the Add method, and the second time I pass a delegate referencing the Subtract method. Inside PerformMathOperation the code will invoke whatever method the delegate is referencing, collect the result and print it to the console. The output will look like this:
9
1
 
We can clearly see how the program has first called Add, and then Subtract. This is in a nutshell what delegates are all about; instead of passing a value you can pass a method that will carry out some action when invoked and optionally return a value.

enter image description here 

In the color coded code above, I have inserted some highlights to clarify some elements of the code. Orange is where delegates are created, getting a reference to a method. Blue is where the delegate is invoked (effectively executing the method it is referencing). Purple is the actual method body of the methods being called through delegates, and yellow are the parameters for those methods. 

As you can see, a method is not executed when becoming referenced by a delegate, but when the delegate is invoked. This is important to keep in mind: delegates are just an object carrying a reference to a method that may be invoked later. As such they can also be stored in variables for later use:
Func<int, int, int> dlgt = Add;

// ... and then later on:
int result = dlgt(5, 4); // result will be 9
 
As I mentioned, the delegates can be created by writing the full form (new Func ...) or a shorter form, leaving some of the work to the compiler. This has no effect of any kind on the resulting code. The same goes for invoking the delegate. In the previous code sample, the delegate variable is used as if it was a method like any other method. Again, there is some compiler magic behind the curtains. In fact, the following three code blocks will result in the exact same code coming out from the compiler:
// first
Func<int, int, int> dlgt = Add;
int result = dlgt(5, 4);

// second
Func<int, int, int> dlgt = new Func<int, int, int>(Add);
int result = dlgt(5, 4);

// third
Func<int, int, int> dlgt = new Func<int, int, int>(Add);
int result = dlgt.Invoke(5, 4);
Which one of these you choose to use is entirely a matter of style.

 

Lambdas an anonymous methods


One of the most common places where you get in touch with delegates are when using LINQ. The following code sample illustrates a common use-case:
static void Main(string[] args)
{
    IEnumerable<string> inputData = new[]
    {
        "Jimi Hendrix",
        "Janis Joplin",
        "Curt Cobain"
    };

    IEnumerable<string> result = inputData.Where(s => s.Contains("x"));
    foreach (var item in result)
    {
        Console.WriteLine(item);
    }
}
 
I have a sequence of strings, called inputData and want to filter out all strings containing the letter "x", so I use the LINQ extension Where to do this:
inputData.Where(s => s.Contains("x"));
 
Let's look at a color-coded version of that statement to compare it to the delegate usage samples above: 
enter image description here

Again, orange is where the delegate is created, purple is the method body of the code being executed and yellow are the parameters. In contrast to the previous samples, we use an anonymous method, so the delegate creation and method declaration happens in the same place, but the elements are still the same.

Somewhat simplified we can say that whatever appears on the left-hand side of => is equivalent to the parameters of a method declaration (whatever appears within the paranthesis), while whatever appears on the right-hand side of => is the method body.

Also, just as in previous examples the C# compiler takes care of some of the "heavy lifting", figuring some things out for so I don't have to write them. I'll get back to that later.

If you check how the Where method is declared, it looks like this:
public static IEnumerable<tsource> Where<tsource>(
    this IEnumerable<tsource> source,
    Func<tsource , bool> predicate
)
 
As you can see, this is an extension method for IEnumerable<T> (that is revealed by the fact that the method is static, and that the first parameter is declared with the this keyword), so it can be called for any IEnumerable<T>, such as our IEnumerable<string> inputData.

The second parameter is a Func<tsource, bool>. This is a delegate type, that refers a method taking one parameter of type tsource, and that returns a bool.

Like with "invisible" delegate declaration above, the C# compiler kindly helps out here as well. Looking at the declaration of the Where extension method, you can see that it has a generic type argument: Where<tsource>. The type argument is inferred by the compiler, so you typically do not need to type it into your code.

The statement inputData.Where(s => s.Contains("x")); is identical to inputData.Where<string>(s => s.Contains("x")); but since inputData is a sequence of strings, the compiler can figure out what type argument to use.

In our example (which is the typical way of writing a simple lambda expression when using LINQ), we have left it up to the compiler to fill in quite a few blanks. If we were to provide the full code, and without the lambda operator, it would look like this instead:
inputData.Where(new Func<string, bool>(delegate(string s) { return s.Contains("x"); }));

While this looks drastically different, there are elements that we can recognize: the parameter s in the method declaration, the s.Contains("x") in the method body. In this code sample, they play exactly the same role as they do when constructing the code using the lambda operator.

Let's transform this somewhat chatty code back to the very short lamdba expression, step by step. First let's use the lambda operator to remove the explicit delegate declaration:
inputData.Where((string s) => { return s.Contains("x"); }));
 
Now it becomes evident that what we do is to essentially remove all code that is about declaring the delegate, but we keep the parameter declaration and the method body, separated by the lambda operator.

Next, we can let the compiler infer the type of the input parameter:
inputData.Where((s) => { return s.Contains("x"); });
 
Since we operate on a list of strings, the input to the method must obviously be of the type string. Next, since the method body consists of only one line, we can remove the curly braces.

Also, when destilling the method body in the lambda expression down to a one-liner, we can (and even must) remove the return keyword:
inputData.Where((s) => s.Contains("x"));
 
Finally, since the number of parameters to the method is one, we can remove the paranthesis around it:
inputData.Where(s => s.Contains("x"));
 
All these code snippets result in the same code.

As you can see, there are a few different ways you can write code that involves declaring and using delegates, but they all end up rendering almost the exact same kind of code in your application, and there is no magic going on, just a helpful compiler making some things easier.

Comments