Monday, March 2, 2009

New Features in C# 3.0 / .NET 3.5 Part 2

This is part 2 of the New features found in C# 3.0 / .NET 3.5. The first post can be found here.

The next thing I'd like to cover is something called "Lambda Expressions". This is a bit more of a complicated one, so I'd like to first start by giving a background on Predicates and Anonymous Functions.

Let's start out again with our Person class from the previous post:

   54         public class Person
   55         {
   56             public string FirstName { get; set; }
   57             public string LastName { get; set; }
   58             public int Age { get; set; }
   59         }

Now let's load up a List with a few Person objects.

   27             List<Person> people = new List<Person>
   28             {                
   29                     new Person { FirstName = "Alex",
   30                     LastName = "Friedman", Age = 27 },
   31                     new Person { FirstName = "Jack",
   32                     LastName = "Bauer", Age = 45 },
   33                     new Person { FirstName = "Cloe",
   34                     LastName = "O'Brien", Age = 35 }
   35             };

The List class in .NET has a FindAll method which takes a Predicate and returns a List. Let me explain:

   19 public delegate bool Predicate<T>(T obj)

That's the signature for the Predicate delegate. Basically, it takes as a parameter an object of type T and returns a bool. So here's a simple use of the find all method using a Predicate. Let's assume we want to find all the people in our List that are older than 30. Here's how we'd use the FindAll method to accomplish this.

First, let's write a method that takes a Person object, and returns a true/false if the person is older/younger than 30:

   47         private bool IsOlderThan30(Person p)
   48         {
   49             return p.Age >= 30;
   50         }

Nothing fancy, simple method. Now, let's create a Predicate that will reference this method:

   56 Predicate<Person> olderThan30 = new Predicate<Person>(IsOlderThan30);

So now, at this point, our olderThan30 Predicate "points" to our IsOlderThan30 method. Now, we can call the FindAll method on our List:

   68 List<Person> result = people.FindAll(olderThan30);

This will return a List of two Person objects. One for "Jack Bauer" who we listed as 45, and one for "Cloe" who is 35. I think it's helpful at this point to understand how this works. Let's take a look at Reflector (if you don't have Reflector, get it now! That thing is awesome. You can grab it here.) to see what the FindAll method looks like under the covers:

   54         public List FindAll<T>(Predicate<T> match)
   55         {
   56             if (match == null)
   57             {
   58                 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
   59             }
   60             List<T> list = new List<T>();
   61             for (int i = 0; i < this._size; i++)
   62             {
   63                 if (match(this._items[i]))
   64                 {
   65                     list.Add(this._items[i]);
   66                 }
   67             }
   68             return list;
   69         }

Basically, it loops through all the items in the collection, and passes each one of them to the method that we passed in as a Predicate. Confused yet? We passed in a Predicate which pointed to a method that took a Person object and returned a bool. The FindAll method, takes each of our Person objects, passes it to our IsOlderThan30 method (that we referenced through our Predicate) and grabs the result. If it returns true, it adds it to a list and then returns that List. A bit confusing, but it's kinda cool actually.

This is all straightforward delegate stuff. Now, here's what .NET 2.0 brough to the table; something called "Anonymous Methods". The idea is simple: We will never use the IsOlderThan30 method ourselves, so why bother writing out a whole method. Anonymous Methods allow us to create the entire method right there when passing it into the FindAll method. Let me demonstrate:

   70             List<Person> result = people.FindAll(delegate(Person p)
   71             {
   72                 return p.Age >= 30;
   73             });

And that's all. We can now eliminate our IsOlderThan30 altogether, and do it all inline. The compiler turns that into an actual method behind the scenes, and then when FindAll is called, it calls that method for each object in this list.

Now, in .NET 3.5 there's something called "Lambda Expressions". Basically, Lambdas are Anonymous Functions, but with an even more tidy syntax. Here's the FindAll method using Lambdas:

   70             List<Person> result = people.FindAll(p => p.Age > 30);

The "=>" is the lambda operator. The "p" is the parameter that will be passed to our anonymous function. It's a bit confusing at first, but it's basically an anonymous function where the compiler can infer the type. Since the FindAll method requires a Predicate where T is a Person object, the compiler can figure out that the "p" being passed to the Lambda is of type Person.

I think another example would help. Say we have a Windows Form with a button on it. On button_click we want to show a MessageBox with today's Date. Here's how we'd normally do it:

   16         public Form1()
   17         {
   18             InitializeComponent();
   19             this.button1.Click += new System.EventHandler(this.button1_Click);
   20         }
   22         private void button1_Click(object sender, EventArgs e)
   23         {
   24             MessageBox.Show(DateTime.Now.ToShortDateString());
   25         }

It's useful again to go back to the basics and understand what's happening here. The Click Property of the Button class is an event which is basically a delegate. We then assign it a new delegate of type EventHandler that's pointing to a function called "button1_Click". When the event is "raised" (when the delegate is called) it calls our function. Here's how we can rewrite this using Lambda Expressions:

   20             this.button1.Click += (o,e) =>
   21             {
   22                 MessageBox.Show(DateTime.Now.ToShortDateString());
   23             };

The thing to note about the syntax is this. If there are no parameters being passed, you do this:

   20 () => //empty parentheses

If there's only one parameter, then parentheses are optional. If there are two or more parameters, then parentheses are required.

To summarize, it's crucial to have a good understanding of delegates and anonymous methods before messing with Lambda Expressions. However, they're used all over LINQ (which I hope to cover in future posts) so I suggest you mess around with these to get a good understanding of them.

No comments: