New features in C# 2.0

August 20th, 2010 Add Your Comments

In this post I will try to present the new things that were introduced in C# 2.0, by creating a short overview which should be able to bring anybody up to speed on them. Readers should be familiar with C# 1.0.

Generics

This topic is too big to be covered in this document. Check the MSDN for details on it. You can also check the following links for more details:

Also note, that with time, content related to generics might be added to this section, however without canceling the above notes.

Variance, covariance on generics

C# does not support variance for generic types, that is you cannot cast from type MyGenType<Type1> to MyGenType<Type0>, even if Type1 inherits from Type0.
Example:

List<int> ints = new List<int>();
ints.Add(1);
ints.Add(10);
ints.Add(42);
List<object> objects = new List<object>();

// doesn't compile ints is not a IEnumerable<object>
//objects.AddRange(ints);

Note
I use the term variance because this is term used in MSDN when talking about generics. However covariance is used more often in the technical literature.
Searching on the internet I couldn’t find a definition for covariance that would suit very well the C# language but, after reading from different sources, I could come out with the following one: in oop we say we have covariance when different constructs that depend on certain types reflect the inheritance direction of those types.
For example C#, starting from version 1.0, supports arrays covariance, which means that an array of strings is an array of objects, as a string is an object:

string[] arrStr = new string[] {"one", "two", "three"};
object[] arrObj = arrStr;
arrObj[1] = "Hello";

However, array covariance applies only to reference types. Arrays to value types are invariant. The following example will fail to compile:

int[] arrInt = new int[] {1, 2, 3};
object[] arrObj = arrInt;

Stating from C# 2.0, covariance is available for delegates. This might be a hint that it might also be available in the future for generics.

Nullable types

Provide a mechanism to assign the null value to value types. This is especially useful when working with databases. The main ideas behind nullable types are:

  • Nullable types are expressed using the Nullable<T> generic structure. Example:
    Nullable<int> ni1 = null;
    Nullable<int> ni2 = 10;
  • There is shorthand notation for nullable types, consisting in adding a ‘?’ sign after the value type. Example: Nullable<int> is equivalent to int?.
  • Nullable types work by holding two values: the value of the underlying type and a bool value indicating if the instance is null. This means that when you use a nullable type you use more memory than when you use the underlying type directly. Also, using a nullable type might be slower because of the conversions made in the different operations.
  • Only value types have a nullable representation. Reference types accept null as a value by their own nature. For example the statement string? nstr; will generate a compiler error.
  • There is implicit conversion from a value type to its nullable representation. For example, the following statements are valid:
  • int  i = 12;
    int? ni = i;
  • There must be an explicit cast when assigning a nullable type to its underlying type. If the nullable instance is null and it is casted to the underlying type, a runtime exception is generated. See the ?? operator to sea how to cast nullable types to their underlying type.
  • Operators can be used directly on nullable types instances if the underlying type supports them. For example:
    int? ni1 = null;
    int? ni2 = 2;
    int? ni3 = 3;

    Adding two instances of int? will have as result another int? instance.

    ni1 + ni2 // equals null.
    ni2 + ni3 will return an int? // instance with the value of 5.
    
    // ----------------------------------------------------
    Color? nc1 = Color.Red;
    Color? nc2 = Color.Blue;
    Color? nc3 = nc1 + nc2 // will raise a compiler error because the Color structure doesn't implement the addition operator.
  • When boxing a nullable type the underlying type is boxed. For example:
    int? ni = 5;
    object obj = ni;
    Console.WriteLine(ni.GetType() == typeof(int)); // will output true
  • When comparing two instances of a nullable type, the comparison is actually made on the underlying type, if both values are not null. The actual comparison is made using the Nullable utility class. Check for it in the MSDN for details on the Compare and Equals methods.

?? operator (coalesce operator)

It has the following syntax: result = left_val ?? right_val; and it means: assign left_val to result when left_val is not null or assign right_val to result when left_val is null.
The ?? operator is especially useful when left_val is a nullable type and result is of its underlying type. In this case, if left val is not null, it is casted to its underlying type and assigned to result.
Example:

int? ni = 3;
int i = ni ?? -1;  // assigns 3 to i. If ni would have been null, -1 would have been assigned to i

Static classes

Are declared using the static keyword and denote classes that cannot be instantiated and can only have static fields and methods. Example:

private static class Program() 
{
  private static void Main() { Console.WriteLine("Hello world!"); }
}

Properties get/set access modifiers

Starting from C# 2.0, setting different access modifiers for the get/set part of a property is possible. Example:

public string Message
{
    get { return _message; }
    protected set { _message = value; }
}

Partial classes

Are used to declare the same class in multiple source files. This is useful when using code generators. Partial classes follow the following rules:

  • A partial class is declared using the partial keyword, which has to be placed immediately before the class, struct or interface keywords.
  • All partial-type definitions meant to be parts of the same type must use the partial keyword in their definition.
  • All parts from a partial class must be defined in the same assembly.
  • Access modifiers, generic constraints, base class and interface implementation declarations, the abstract, sealed, static and new keywords can be used on any partial definition as long they do not conflict with each other. For example:
    public partial class MyClass : MyBase, Interface1
    { }
    
    abstract partial class MyClass : Interface2
    { }

    is equivalent with:

    public abstract class MyClass : MyBase, Interface1, Interface2
    { }
  • Inner classes can be partial.

Delegate covariance

For more details on covariance check the note, from variance section in Generics. By using covariance, delegates take into account the inheritance properties of the types they contain. The following example reflects that:

private delegate object GetObjectDelegate(); // returns object

private static string GetString() // returns string which inherits object
{
    return "Hello world!";
}

private static void Main()
{
    // allowed by delegate covariance
    GetObjectDelegate objectGetter = new GetObjectDelegate(GetString);
    Console.WriteLine(objectGetter());
}

Anonymous methods

Are used to directly attach blocks of code to delegates and represent another step forward towards functional programming in C#.
Here is an example of how to declare an anonymous method:

MethodInvoker anonymousMethod = delegate
{
    Console.WriteLine("Hello from anonymous method");
};

anonymousMethod();

Here are some rules for anonymous methods:

  • You declare an anonymous method by using the delegate keyword, followed by the block of code representing the method’s body, folowed by a colon.
  • An anonymous method must be assigned to a delegate. The method can only be invoked by invoking the delegate.
  • An anonymous method can use input parameters that will have to be declared immediately after the delegate keyword. Example:
    EventHandler anonymousMethod = delegate(object sender, EventArgs e)
    {
        Console.WriteLine(sender.GetType().ToString());
    }
  • An anonymous method is strong typed since it gets its type from the delegate it is being assigned to. However, if the method doesn’t use any parameters in its body, it can omit enumeration them after the delegate keyword. For example, the following statement is valid:
    // The EventHandler handler delegate requires two parameters, but since  
    // they are not used in the body of the method, they have been omitted 
    // from the declaration of the anonymous method.
    EventHandler anonymousMethod = delegate
    {
        Console.WriteLine("Hello world");
    }
  • There is no way you can specify the return type in the declaration of an anonymous method. It will be retrieved from the delegate the method is assigned to. This is the reason why in C# 3.0 you cannot assign an anonymous method to var.
  • While you can declare anonymous methods, you cannot declare anonymous delegates, that is declare a delegate by its return type and parameters without naming it. This means you will always need to have a delegate type defined for every type of anonymous method you need to use. To avoid declaring delegates that are going to be used in only one place, the framework provides a series of generic delegates, that can be found under the Func and Action names. Func generic delegates are for methods that return a value, while Action delegates are for methods that return void. Here is the complete list of Action and Func delegates:
    public delegate void Action();
    public delegate void Action<T1>(T1 arg1);
    public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
    public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
    public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3,T4 arg4);
    
    public delegate TResult Func<TResult>();
    public delegate TResult Func<T1, TResult>(T1 arg1);
    public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
    public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
    public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
  • Another delegate with a specific meaning defined in the framework is the Predicate delegate. The Predicate delegate is intended to be used for checking when an object corresponds to a criteria defined in the method assigned to the delegate.
    public delegate bool Predicate<T>(	T obj);
  • Anonymous methods allow the use of closures in C#. Example:
    private void Test2()
    { 
        MethodInvoker counter = this.GetCounter();
        for (int i = 0; i < 10; i++)
            counter();
    }
    
    private MethodInvoker GetCounter()
    {
        int val = 0;
        MethodInvoker counter = delegate { Console.WriteLine("Value = {0}", val++); };
    
        return counter;
    }

Method Group Conversions

It is more a shorthand than a new feature consisting in the fact that you can assign the name of a method directly to a delegate or event without explicitly initializing the delegate with the new keyword. Example:

private void Test3() {
    // no need for 'new Action(PrintMessage)'
    Action action = PrintMessage; 
    action();
}

private void PrintMessage()  {
    Console.WriteLine("Hello world!");
}

yield keyword (iterator methods)

In C# 1.0, a class must implement the IEnumerable interface in order to offer foreach functionality. Starting with C# 2.0, iterator methods provide an easier way to implement foreach functionality.
Here are some of the properties of iterator methods:

  • Iterator methods use the yield keyword, which can be found in two flavors: yield return <expression>, and yield break. Yield return is called for every element of the iteration, in the order of the iteration. Yield break is called to signal that the iteration is finished. Example:
  • public class MyClass
    {
        private string[] _messages = new string[] {"one", "two", "three"};
    
        public IEnumerator GetEnumerator()
        {
            foreach (string str in _messages)
                yield return str;
        }
    }
    
    class Program
    {
        private static void Main()
        {
            MyClass myClass = new MyClass();
            foreach (string str in myClass)
                Console.WriteLine(str);
        }
    }

    which will output:

    one
    two
    three
  • All iterator methods must have at least one yield return statement. Yield break doesn’t have to be present.
  • The yield return statement doesn’t have to be in a loop. Example:
    public IEnumerator GetEnumerator()
    {
        yield return "one";
        yield return "two";
        yield return "three";
    }
  • When the compiler encounters an iterator method it will generate a nested class and implement the GetEnumerator(), MoveNext() and Current methods. However, it will not generate the Reset() method for the nested class, and calling that method will trigger a runtime exception.
  • An iterator method does not execute head to tail to cache all the elements of the iteration in a temporary collection that will be used by the foreach statement. Instead, when MoveNext() is called on the iterator, the iterator method will execute until it reaches the first yield return. This is implemented using labels and goto statements. The mechanism is pretty fast, which means that using iterator methods is ok from the performance point of view.
  • Classes that contain an iterator method do not have to implement the IEnumerable interface. The compiler and the CLR will know that they can use the iterator method to provide foreach functionality. However, there is no problem if you wand to declare the class IEnumerable, with the limitation that you will not be able to call Reset() on its IEnumerator.
  • An iterator method that has the GetIterator name and returns an IEnumerator, like the one from the example above, is called an unnamed iterator and there can be only one such iterator in the class. However, you can also declare named iterators, which return an IEnumerable and are explicitly called in the foreach statement. Example:
    private static void Main()
    {
        MyClass myClass = new MyClass();
        foreach (string str in myClass.GetHelloMessages())
            Console.WriteLine(str);
    }
    
    public class MyClass
    {
        private string[] _messages = new string[] { "one", "two", "three" };
    
        public IEnumerable GetHelloMessages()
        {
            yield return "Hello world!";
            yield return "Bonjour!";
            yield return "Salut!";
        }
    }
  • Iterators can be defined as methods, operators or get properties.
  • Iterator methods cannot have unsafe blocks in their body.
  • Iterator methods cannot have ref or out parameters.
  • Yield statements cannot appear in anonymous methods.

Comments are closed.