Extension Methods: Behind the scenes
One of the sessions I was able to attend (and talk about) at the MVP Summit involved some of the new features of C# 3.0. My day job still has me doing a lot of C# 2.0 stuff so I haven't dived in too deeply to the new 3.0 stuff. This was my opportunity to get the information directly from the C# language PM Mads Torgersen. One thing I found interesting was how extension methods were implemented.
If you're not familiar with extension methods, here they are in a nutshell (and simplified): Extension methods allow you to add new methods to existing classes -- classes that you don't have source code access to and can't recompile with the new method. In reality, you're not truly adding new methods, but the IDE experience makes it look that way so it flows very nicely.
First, let's look at how we'd tackle a problem without extension methods. We have a simple Customer class. I've used C# 3.0's auto-properties to allow me to quickly define the class:
1: public class Customer
2: {
3: public int ID { get; set; }
4: public string Name { get; set; }
5: public double Balance { get; set; }
6: }
Now let's create a list of customers since we're going to iterate over them later:
1: static List<Customer> GetCustomers()
2: {
3: return new List<Customer>()
4: {
5: new Customer {ID = 1, Name="Bill", Balance = 22.50},
6: new Customer {ID = 2, Name="Bob", Balance = 0.00},
7: new Customer {ID = 3, Name="Joe", Balance = -5.00}
8: };
9: }
Note that in the GetCustomers code above I'm utilizing C# 3.0 property initializers too to make this demo simple and concise.
Now we get to the meat of the problem. First off, we're assuming that the Customer class is in another DLL that we don't have source code for it. We'd like to know which customer's are delinquent, i.e. a balance less than zero. It would be easy to simply loop through and check the Balance property, but we'd need that check everywhere we look for a delinquent customer (reports, inquiries, lookups, etc...). And what if our client decided to change the definition of "delinquent" (for example "a balance less than -10.00)? We'd have to change that balance check everywhere. Instead, we'll create a utility method for this (since we can't simply add it to the Customer class):
1: public static class MyExtensions
2: {
3: public static bool IsDelinquent1(Customer c)
4: {
5: return c.Balance < 0;
6: }
7: }
(You'll see why I called it "IsDelinquent1" and not simply "IsDelinquent" shortly). We created a static class and created a utility method that will tell us if a customer is "delinquent" (based on our client's current definition). Now let's do a quick check to find delinquent customers:
1: private static void Method1()
2: {
3: var customers = GetCustomers();
4:
5: foreach (Customer c in customers)
6: {
7: if (MyExtensions.IsDelinquent1(c))
8: {
9: Console.WriteLine(c.Name);
10: }
11: }
12: }
Pretty simple. However, it is a little verbose. Let's now look at how we can utilize extension methods to make this cleaner.
We go back to our static "MyExtensions" class and add our extension method. It's important to note that extension methods can only be defined in static classes that are contained directly in a namespace (i.e. not a nested class).
1: public static bool IsDelinquent2(this Customer c)
2: {
3: return c.Balance < 0;
4: }
This looks almost exactly like the first method (the code block is identical). We've simply added the "this" keyword at the beginning of the parameter list (before the "Customer" reference). This tells the C# compiler (and the VS.NET IDE) that "IsDelinquent2" is an extension method on "Customer" since it appears right before a Customer parameter. That's the only difference!
Utilizing this extension method makes the loop code much cleaner and easier to read:
1: private static void Method2()
2: {
3: var customers = GetCustomers();
4:
5: foreach (Customer c in customers)
6: {
7: if (c.IsDelinquent2())
8: {
9: Console.WriteLine(c.Name);
10: }
11: }
12: }
Very nice!
Now the reason I've got static code all over this demo is because you can take all of the code, slap it into a VS2008 Console application and compile it (make sure the MyExtensions class is not nested within the default "Program" class). Now fire up ILDASM and look at the decompiled IL for both "Method1" and "Method2" (I named them differently so you could compile them together and examine the differences). You'll see that the IL code is exactly the same, byte for byte. That's because extension methods are not a CLR addition, they are a C# addition. They're a neat "macro" that the C# compiler automatically does for you.
It's a pretty neat implementation and can make interaction with external libraries a bit easier. And if you're wondering, yes, the extension methods are listed as a method directly on the class with Intellisense. They get a slightly different icon (I wasn't able to capture it with my screen capture utility) and the tooltip for the method in Intellisense has "(extension)" at the beginning of the method name.