In the previous post I mentioned the whole language integrated query thing. As it turns out, the query part is not actually the part where the most ‘magic’ occurs. Having the query functionality really relies on new additions to the C# language.
For instance if we look at the following query:
var contacts =
from c in customers
where c.State == “WA”
select new { c.Name, c.Phone };
this is just converted by the compiler to:
var contacts =
customers
.Where(c => c.State == “WA”)
.Select(c => new { c.Name, c.Phone });
So now you might think, well I still see a lot of stuff that is not something I can do in C# right now. Let’s go through it by feature.
Local variable type inference
This is basically the ‘var’ keyword. Don’t worry, it doesn’t mean variant, object, or latebound stuff all over again. In fact, it just saves some typing (not taking anonymous classes into account). What it does is to automatically take the type from the expression on the right-hand side.
For instance:
var i = 5;
is exactly equivalent with:
int i = 5;
This is because the compiler knows that the right-hand side is an expression of type int.
Lambda expressions
The next thing to notice is the => syntax in c => c.State == “WA”. This is known as a Lambda expression and is basically a shorthand construct that deals with anonymous delegates in an elegant way.
In this case, the above expression translates to:
delegate(Customer c) { return c.State == “WA”; }
So what the compiler does here, is to take the Item type of the Customers collection and apply that to the c variable that is used as input for the Lambda expression.
Extension methods
You might think that to make this all work, the customers collection needs to derive from some special type that implements the Where method. This is actually not the case. Any type that supports IEnumerable<T> can be used as a source for the query.
This is possible thanks to the feature of Extension methods.
What this feature allows for, is to add behaviour to existing classes. The way this is done is by introducing the this modifier for a parameter.
In the code below the Where and Select extension methods for and IEnumerable<T> are defined.
namespace System.Query
{
public static class Sequence
{
public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
Func<T, bool> predicate) { … }
public static IEnumerable<S> Select<T, S>(this IEnumerable<T> source,
Func<T, S> selector) { … }
…
}
}
This makes it possible to create syntax as shown below:
using System.Query;
IEnumerable<string> contacts =
customers.Where(c => c.State == “WA”).Select(c => c.Name);
The way the compiler makes it works is to not directly report an error when a method isn’t found, but to try an alternative search method.
Let’s say the compiler tries to find XXX.Foo(int), which does not exist. It will then start looking for a public static function that conforms to the signature Foo(XXX, int). When this function is found, the code is simply generated to use the static function. To make it efficient, not all available assemblies and namespaces are searched using this method. Only the namespaces defined by the using keyword are searched.
However, if you have a method that does implement the Where function natively, it will be used, because it will not start searching for extension methods. In this way, the where functionality can be tuned for specific collection types.
Anonymous types
Then we have some more features J. A part of the syntax is:
.Select(c => new { c.Name, c.Phone })
A new thing here is the new {} construct, which is called an anonymous type. What it does is automatically generate a new class definition. In this case that class will have two public fields, called Name and Phone.
The feature was introduced so that it now is possible to specify a ‘select list’ without having to explicitly declare a class to hold those projected results. In essence, the result set is nothing else than a collection of ‘row objects’ and it would be cumbersome to define such an object for each select list you create.
Now the power of the var keyword becomes even more apparent. Without var, anonymous types would be impossible, because they do not have a name and thus cannot be declared.
Object Initializers
The other part of the select list is that the anonymous type is initialized using an expression. Note that this anonymous object only has a default constructor and some properties. Yet it is still possible to initialize using an expression.
The trick here is basically the same as with the .NET Attribute syntax, where it is also possible to initialize properties in an expression.
Consider the following class:
public class Point
{
private int x, y;
public int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y = value; } }
}
This class can be initialized in the following way:
Point a = new Point { X = 0, Y = 1 };
The above code is equivalent with:
Point a = new Point();
a.X = 0;
a.Y = 1;
Wrap Up
So as you can see, all of the above language extensions are the real power behind language integrated query, and you can leverage them to more productively write code and do really powerful stuff.
One comment
Great article. Anonymous Types, Anonymous Local Variables, Labda Expressions and Extension Methods ceirtanly fulfill each other. They are all needed to make the new functionality of C# 3.0 come to life.
I still miss async methods and multiple inheritance though.
Kjetil Kristoffer Solberg