Introduction to LINQ: Part I - Delegates

Table of Contents

Resources


Recently I had to explain Language Integrated Queries (LINQ) to a friend that had never used it before. Part way through the explanation I realized that I had been using LINQ daily without a complete understanding of its inner-workings. My verbal tutorial began to crawl as I had to pause and research things for myself in order to better explain them. I decided that a blog post on the subject would not only benefit other newcomers to the world of LINQ but would help organize things in my head and give myself a better understanding of the tool I've already been using extensively. So without further ado, let's get into it.

Part I - Delegates

In order to understand LINQ we must first have a decent understanding of lambda expressions and extension methods. I first want to cover lambda expressions but in order to cover those you must understand what a delegate is and how they are used in .NET. If you already know what delegates are then feel free to skip to the next section.

The easiest way to start explaining delegates is to take the short description directly from the MSDN.

A delegate is a type that references a method. Once a delegate is assigned a method, it behaves exactly like that method. The delegate method can be used like any other method, with parameters and a return value, as in this example:

public delegate int PerformCalculation(int x, int y);

The easiest way that I have found to wrap your head around a delegate is to think of it as a variable that points to a method. What method it points to is defined when you instantiate the variable. You can then call this variable like a method, exactly the same way you would if you were calling the real method that was assigned to the delegate. Of course, as stated in the above quote, a delegate is not a variable it is a type, but thinking of it this way seemed to help me understand them better.

Here is a brief example of how we might implement the delegate described in the above quote from the MSDN. First we have to define a method or methods that we could assign to the delegate.

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

public static int MultiplyNumbers(int x, int y)  
{
    return x * y;
}

In order for a method to be assigned to a delegate, they must both specify the same return type as well as accept the same arguments. Notice that PerformCalculation, AddNumbers(), and MultiplyNumbers() all accept two integers and return an integer, only differing in what it does with the two integers within the method.

So now we have a delegate and two methods that could potentially be assigned to it. Lets see an example of our delegate in action in this sample console application.

static void Main(string[] args)  
{
    PerformCalculation blackBox = AddNumbers;

    Console.WriteLine("Result: {0}", blackBox(9000, 1));
    Console.ReadLine();
}

What did we just do here? Well first we created a new variable called blackBox of type PerformCalculation and assigned it............a method!? Yep, we assigned it a method. Once we created our blackBox we can then use this variable just like a method, passing in two integers as defined by the delegate and the method being assigned to the delegate. Here is the result of running this console application:

Hopefully you are starting to see the power of being able to assign the logic of a method to a variable that could then be passed all over your application. In the above example if you wanted to multiply the numbers passed into blackBox you would simply assign MultiplyNumbers when instantiating the delegate, replacing AddNumbers.

This use of delegates has been available since the very first iterations of .NET, but before we can move on to lambdas we need to understand another way of declaring a delegate that was introduced in .NET 2.0. Anonymous methods.

The limitation of the above delegate approach is that you must create a named method for every piece of logic you want the delegate to be able to perform. Usually the purpose of a delegate is to encapsulate some logic and pass it around the application and execute it. Creating named methods gives the unnecessary presence of real methods that could also be called on the class they are defined. What if you only want to use those methods with a delegate? Well now you can with anonymous methods. Here is an example using the same console application we created above:

class Program  
{
    delegate int PerformCalculation(int x, int y);

    static void Main(string[] args)
    {
        PerformCalculation blackBox = delegate(int x, int y)
        {
            return x + y;
        };

        Console.WriteLine("Result: {0}", blackBox(9000, 1));
        Console.ReadLine();
    }
}

Notice how we instantiated the delegate and included a statement block below it? We no longer had to write a named method. The result of running this console application is the same result as the first one, but it was accomplished a little differently. There are no longer named static methods on our Program class. You would just change the logic in the statement block to change what happens when blackBox gets invoked.

Why is this useful? Lets look at anonymous methods in a more practical usage. Modify your console application to look something like this:

class Program  
{
    delegate bool EvaluateCondition(string value);

    static void Main(string[] args)
    {
        // Create a list of sci-fi shows.

        List<string> scifiShows = new List<string>
        {
            "Star Trek: The Next Generation",
            "Star Trek: Voyager",
            "Star Trek: Deep Space Nine",
            "Battlestar Gallactica",
            "Doctor Who",
            "Stargate SG-1",
            "Stargate Atlantis",
            "Stargate Universe",
            "Futurama"
        };

        // Get a filtered list of shows by calling FilterList() and passing in our 
        // list of shows and a delegate to be used when checking values to be filtered.
        // The delegate uses the string method .StartsWith() to check if the string starts
        // with a certain value.

        List<string> filteredList = FilterList(scifiShows, delegate(string value)
        {
            return value.StartsWith("Star Trek");
        });

        // Print each show in the filtered list to the console.

        foreach (string show in filteredList)
        {
            Console.WriteLine(show);
        }
        Console.ReadLine();
    }

    /// <summary>
    /// Filters a string list based on a delegate function used to check each value in the list.
    /// </summary>
    /// <param name="list">The list to be filtered.</param>
    /// <param name="evaluateCondition">The delegate to be invoked when checking each value.</param>
    /// <returns>The filtered string list.</returns>
    static List<string> FilterList(List<string> list, EvaluateCondition evaluateCondition)
    {
        List<string> filteredList = new List<string>();

        foreach (string show in list)
            if (evaluateCondition(show))
                filteredList.Add(show);

        return filteredList;
    }
}

Note that .StartsWith() is a string method used to check if a string starts with a certain value. If you've never seen that method before, that's all it is.

Seems like a fair amount of code but what we did was simple. We created a string list of science fiction shows and then passed into the FilterList method along with an anonymous method. The anonymous method is assigned to the delegate. The filter method loops through our string list and passes each value into the delegate method. If the delegate returns true then it adds the value to a new string list, the filtered list. Here is the result:

The beautiful thing about this approach is that the FilterList method has no idea what happens inside the delegate. All it knows is that it passes in a string and gets a true or false, it doesn't care what logic was used inside there to come to that conclusion.

Now that you have a decent understanding of delegates and how they work, we can finally move into lambda expressions.

Part II - Lambda Expressions