What's new in C# 3.0: Primer

The following extracts from C# 3.0 in a Nutshell explain four of the new features of C# 3.0: Lambda Expressions, Extension Methods, Implicit Typing, and Anonymous Types. These are the core features upon which LINQ queries build.

Lambda Expressions

A lambda expression is an unnamed method written in place of a delegate instance. The compiler immediately converts the lambda expression to either:

Given the following delegate type:

delegate int Transformer (int i);

we could assign and invoke the lambda expression x => x * x as follows:

Transformer sqr = x => x * x;
Console.WriteLine (sqr(3));    // 9

Internally, the compiler resolves lambda expressions of this type writing a private method, and moving the expression’s code into that method.

A lambda expression has the following form:

(parameters) => expression-or-statement-block

For convenience, you can omit the parentheses if and only if there is exactly one parameter of an inferable type.
In our example, there is a single parameter, x, and the expression is x * x:

x => x * x;

Each parameter of the lambda expression corresponds to a delegate parameter, and the type of the expression (which may be void) corresponds to the return type of the delegate.

In our example, x corresponds to parameter i, and the expression x * x corresponds to the return type int, therefore being compatible with the Transformer delegate:

delegate int Transformer (int i);

A lambda expression’s code can be a statement block instead of an expression. We can rewrite our example as follows:

x => {return x * x;};

Explicitly Specifying Lambda Parameter Types

The compiler can usually infer the type of lambda parameters contextually. When this is not the case, you must specify the type of each parameter explicitly. Consider the following delegate type:

delegate int Transformer (int i);

The compiler uses type inference to infer that x is an int, by examining Transfomer’s parameter type:

Transformer d = x => x * x;

We could explicitly specify x’s type as follows:

Transformer d = (int x) => x * x;

Generic Lambda Expressions and the Func Delegates

With generic delegates, it becomes possible to write a small set of delegate types that are so general they can work for methods of any return type and any (reasonable) number of arguments. These delegates are the Func and Action delegates, defined in the System namespace. Here are the Func delegates (notice that TResult is always the last type parameter):

delegate TResult Func <T> ();

delegate TResult Func <T, TResult>
                      (T arg1);

delegate TResult Func <T1, T2, TResult>
                      (T1 arg1, T2 arg2);

delegate TResult Func <T1, T2, T3, TResult>
                      (T1 arg1, T2 arg2, T3 arg3);

delegate TResult Func <T1, T2, T3, T4, TResult>
                      (T1 arg1, T2 arg2, T3 arg3, T4 arg4);

Here are the Action delegates:

delegate void Action();

delegate void Action <T>
                     (T1 arg1);

delegate void Action <T1, T2>
                     (T1 arg1, T2 arg2);

delegate void Action <T1, T2, T3>
                     (T1 arg1, T2 arg2, T3 arg3);

delegate void Action <T1, T2, T3, T4>
                     (T1 arg1, T2 arg2, T3 arg3, T4 arg4);

These delegates are extremely general. The Transformer delegate in our previous example can be replaced with a Func delegate that takes a single int argument and returns an int value:

Func<int,int> sqr = x => x * x;
Console.WriteLine (sqr(3));     // 9

Outer Variables

A lambda expression can reference the local variables and parameters of the method in which it’s defined. For example:

static void Main()
{
   int factor = 2;
   Func<int, int> multiplier = n => n * factor;
   Console.WriteLine (multiplier (3));           // 6
}

Local variables and parameters referenced by a lambda expression are called outer variables or captured variables. A lambda expression that includes outer variables is called a closure.

Outer variables are evaluated when the delegate is actually invoked, not when the variables were captured:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (2));   // 20

Lambda expressions can themselves update captured variables:

int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural());      // 0
Console.WriteLine (natural());      // 1

Outer variables have their lifetimes extended to that of the delegate. In the following example, the local variable seed would ordinarily disappear from scope when Natural finished executing. But because seed has been captured, its lifetime is extended to that of the capturing delegate, natural:

static Func<int> Natural()
{
   int seed = 0;
   return () => seed++; // Returns a closure
}

static void Main()
{
   Func<int> natural = Natural();
   Console.WriteLine (natural()); // 0
   Console.WriteLine (natural()); // 1
}

A local variable instantiated within a lambda expression is unique per invocation of the delegate instance. If we refactor our previous example to instantiate seed within the lambda expression, we get a different (in this case, undesirable) result:

static Func<int> Natural()
{ 
   return() => { int seed = 0; return seed++; };
}

static void Main()
{
   NumericSequence natural = Natural();
   Console.WriteLine (natural());      // 0
   Console.WriteLine (natural());      // 0
}

Extension Methods

Extension methods allow an existing type to be extended with new methods, without altering the definition of the original type. An extension method is a static method of a static class, where the this modifier is applied to the first parameter. The type of the first parameter will be the type that is extended. For example:

public static class StringHelper
{
   public static bool IsCapitalized (this string s)
   {
      if (string.IsNullOrEmpty (s)) return false;
      return char.IsUpper (s[0]);
   }
}

The IsCapitalized extension method can be called as though it were an instance method on a string, as follows:

Console.Write ("Perth".IsCapitalized());

An extension method call, when compiled, is translated back into an ordinary static method call:

Console.Write (StringHelper.IsCapitalized ("Perth"));

Interfaces can be extended, too:

public static T First<T> (this IEnumerable<T> sequence)
{
   foreach (T element in sequence)
      return element;

   throw new InvalidOperationException ("No elements!");
}
...
Console.WriteLine ("Seattle".First());   // S

Extension Method Chaining

Extension methods, like instance methods, provide a tidy way to chain functions. Consider the following two functions:

public static class StringHelper
{
   public static string Pluralize (this string s) {...}
   public static string Capitalize (this string s) {...}
}

x and y are equivalent and both evaluate to "Sausages", but x uses extension methods, whereas y uses static methods:

string x = "sausage".Pluralize().Capitalize();

string y = StringHelper.Capitalize (StringHelper.Pluralize ("sausage"));

Ambiguity and Resolution

Namespaces

An extension method cannot be accessed unless the namespace is in scope (typically imported with a using statement.)

Extension methods versus instance methods

Any compatible instance method will always take precedence over an extension method. In the following example, Test’s Foo method will always take precedence—even when called with an argument x of type int:

class Test
{
   public void Foo (object x) { }    // This method always wins
}

static class Extensions
{
   public static void Foo (this Test t, int x) { }
}

The only way to call the extension method in this case is via normal static syntax; in other words, Extensions.Foo(…).

Extension methods versus extension methods

If two extension methods have the same signature, the extension method must be called as an ordinary static method to disambiguate the method to call. If one extension method has more specific arguments, however, the more specific method takes precedence.

To illustrate, consider the following two classes:

static class StringHelper
{
   public static bool IsCapitalized (this string s) {...}
}

static class ObjectHelper
{
   public static bool IsCapitalized (this object s) {...}
}

The following code calls StringHelper’s IsCapitalized method:

bool test1 = "Perth".IsCapitalized();

To call ObjectHelper’s IsCapitalized method, we must specify it explicitly:

bool test2 = (ObjectHelper.IsCapitalized ("Perth"));

var—Implicitly Typed Local Variables

It is often the case that you declare and initialize a variable in one step. If the compiler is able to infer the type from the initialization expression, you can use the word var in place of the type declaration. For example:

var x = 5;
var y = "hello";
var z = new System.Text.StringBuilder();
var req = (System.Net.FtpWebRequest) System.Net.WebRequest.Create ("...");

This is precisely equivalent to:

int x = 5;
string y = "hello";

System.Text.StringBuilder z = new System.Text.StringBuilder();

System.Net.FtpWebRequest req = 
   (System.Net.FtpWebRequest) System.Net.WebRequest.Create ("...");

Because of this direct equivalence, implicitly typed variables are statically typed. For example, the following generates a compile-time error:

var x = 5;
x = "hello"; // Compile-time error; x is of type int

var can decrease code readability in the case you can’t deduce the type purely from looking at the variable declaration. For example:

Random r = new Random();
var x = r.Next();


What type is x?

Implicit typing is mandatory with anonymous types.

Anonymous Types

An anonymous type is a simple class created on the fly to store a set of values. To create an anonymous type, you use the new keyword followed by an object initializer, specifying the properties and values the type will contain. For example:

var dude = new { Name = "Bob", Age = 1 };

The compiler resolves this by writing a private nested type with read-only properties for Name (type string) and Age (type int). You must use the var keyword to reference an anonymous type, because the type’s name is compiler-generated.

The property name of an anonymous type can be inferred from an expression that is itself an identifier. For example:

int Age = 1;
var dude = new { Name = "Bob", Age };

is equivalent to:

var dude = new { Name = "Bob", Age = Age };

Anonymous types are used primarily when writing LINQ queries.

 

C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription