C# 2.0

The first major update of the C# language, Runtime, and .NET Framework was a big one. This release focused on making the language more concise and easier to write.

Syntax updates

The first update added a small capability to the property syntax. In 1.0, if you wanted a read only property, your only choice was to exclude the setter, as follows:

private int _value;

public int Value
{
    get { return _value; }
}

All internal logic had to interact with the _value member directly. In many cases this was fine, except for cases where you needed to have some sort of logic governing when and how you were allowed to change that value. Or similarly, if you needed to raise an event, you would have to create a private method as follows:

private void SetValue(int value)
{
    if (_value < 5)
        _value = value;
}

Well no more in C# 2.0, as you can now create a private setter as follows:

private int _value;

public int Value
{
    get { return _value; }
    private set
    {
        if (_value < 5)
            _value = value;
    }
}

A small feature, but it increased consistency because separate getter and setter methods were one of the things that C# tried to get rid of from the first version.

Another interesting addition is that of nullable types. With value types, the compiler will not allow you to set them to a null value, however, you now have a new key character that you can use to signify a nullable value type as follows:

int? number = null;
if (number.HasValue)
{
    int actualValue = number.Value;
    Console.WriteLine(actualValue);
}

Just by adding the question mark, the value type is marked as nullable, and you can use the .HasValue and .Value properties to make decisions on what to do in the case of null.

Anonymous methods

Delegates are a great addition to C# over other languages. They are the building blocks of the event systems. One drawback, however, in the way they were implemented in C# 1.0 is that they make reading code a bit more difficult, because the code that executes when the event is raised is actually written elsewhere. Continuing the trend of code simplification, anonymous methods let you write the code inline. For example, given the following delegate definition:

public delegate void ProcessNameDelegate(string name);

You can create an instance of the delegate using an anonymous method as follows:

ProcessNameDelegate myDelegate = delegate(string name)
{
    Console.WriteLine("Processing Name = " + name);
};

myDelegate("Joel");

This code is inline, short, and easy to understand. It also allows you to use delegates much like first-class functions in other languages such as JavaScript. But it goes beyond simply being easier to read. If you wanted to pass a parameter to a delegate that did not accept a parameter in C# 1.0, you had to create a custom class to wrap both the method implementation and the stored value. This way, when the delegate is invoked (thus executing the target method), it has access to the value. Any early multi-threaded code was full of code like the following:

public class CustomThreadStarter
{
    private int value;
    public CustomThreadStarter(int val)
    {
        this.value = val;
    }

    public void Execute()
    {
        // do something with 'value'
    }
}

This class accepts a value in the constructor and stores it in a private member. Then later when the delegate is invoked, the value can be used, as in this case using it to start a new thread. This is shown in the following code:

CustomThreadStarter starter = new CustomThreadStarter(55);
ThreadStart start = new ThreadStart(starter.Execute);
Thread thread = new Thread(start);
thread.Start();

With anonymous delegates, the compiler can step in and greatly simplify the usage pattern mentioned previously as follows:

int value = 55;
Thread thread = new Thread(delegate()
    {
        // do something with 'value'
        Console.WriteLine(value);
    });
thread.Start();

This might look simple, but there is some serious compiler magic going on here. The compiler has analyzed the code, realized that the anonymous method requires the value variable in the method body, and automatically generated a class similar to the CustomThreadStarter that we would have had to create in C# 1.0. The result is code that you can easily read because it is all there, right in context with the rest.

Partial classes

In C# 1.0, it was common practice to use code generators to automate things such as custom collections. When you wanted to add your own methods and properties to the generated code, you would generally have to inherit from the class, or in some cases, directly edit the generated file. This meant that you had to be very careful to avoid regenerating the code, or risk overwriting your custom logic. You will find a comment similar to the following one in many first generation tools:

// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3053
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>

C# 2.0 adds an additional keyword to your arsenal, partial. With partial classes, you can break up your classes among multiple files. To see this in action, create the following class:

// A.generated.cs
public partial class A
{
    public string Name;
}

This represents the automatically generated code. Notice that the file contains .generated in the filename; this is a convention that was adopted, though is not necessary for this to work, it is just that both files are part of the same project. Then in a separate file, you can include the rest of the implementation as follows:

// A.cs
public partial class A
{
    public int Age;
}

All members would then be available on the resulting type at runtime, as the compiler takes care to stitch the class together. You are free to regenerate the first file at will, without the risk of overwriting your changes.

Generics

The major feature addition of C# 2.0 is generics, which allows you to create classes that can be reused with multiple types of objects. In the past, this kind of programming could only be accomplished in two ways. You can use a common base class for the parameter, so that any object that inherits from that class can be passed in regardless of the concrete implementation. That works, sort of, but it becomes very limiting when you want to create a very general purpose data structure. The other method is really just a derivative of the first. Instead of using a base class of your own definition, go all the way up the inheritance tree and use object for your type parameter.

This works because all the types in .NET derive from object, so you can pass in anything. This is the method used by the original collection classes. But even this has problems, especially when it comes to passing in value types due to the effects of boxing. You also have to cast the type back out from object every single time.

Thankfully, all of these problems can be mitigated by using generics as follows:

public class Message<T>
{
    public T Value;
}

In this example, we have defined a generic type parameter called T. The actual name of the generic type parameter can be anything, T is just used as a convention. When you instantiate the Message class, you can specify the kind of object you want to store in the Value property using this syntax, as follows:

Message<int> message = new Message<int>();
message.Value = 3;
int variable = message.Value;

So you can assign an integer to the field without worrying about performance, because the value will not be boxed. You also do not have to cast it when you want to use it, as you would if using object.

Generics are super powerful, but they are not omnipotent. To highlight a key deficiency, we will go over one of the first things that just about every C# developer tried when 2.0 was first released—generic math. Developers of applications that are math heavy will likely be using a mathematical library for their domain. For example, game developers (or really, anyone doing anything that involves 2D or 3D spatial calculations) will always need a good Vector structure as follows:

public struct Vector
{
    public float X;
    public float Y;

    public void Add(Vector other)
    {
        this.X += other.X;
        this.Y += other.Y;
    }
}

But the problem is that it is using the float data type for calculations. If you wanted to generalize it and support other numeric types such as int, double, or decimal, what do you do? Upon first glance, you would think that you could use generics to support this scenario as follows:

public struct Vector<T>
{
    public T X;
    public T Y;

    public void Add(Vector<T> other)
    {
        this.X += other.X;
        this.Y += other.Y;
    }
}

Compiling this will result in an error, Operator '+=' cannot be applied to operands of type 'T' and 'T'. This is because, by default, only members from the object data type are available for the generic parameter, due to the fact that the compiler has no way of knowing what methods (and by extension, operations) are defined on the type you are using.

Thankfully, Microsoft anticipated this to some degree, and added something called generic type constraints . These constraints let you give the compiler a hint at what kind of types callers will be allowed to use, which in turn means that you can use the features that you constrain. For example, look at the following code:

public void WriteIt<T>(T list) where T : IEnumerable
{
    foreach (object item in list)
    {
        Console.WriteLine(item);
    }
}

Here, we have added a constraint that says that the type parameter T must be an IEnumerable. As a result, you can write the code and be safe in the knowledge that any caller that calls this method will only ever use a type that implements the IEnumerable interface as the type parameter. Some of the other parameter constraints you can use are as follows:

  • class: This says that the type parameter must be a reference type.
  • struct: This implies that only value types are allowed.
  • new(): There must be a public constructor without parameters on this type. It will allow you to use syntax like T value = new T() to create new instances of the type parameter. Otherwise, the only thing you can do is something like T value = default(T), which will return null for reference types, and zero for numeric primitives.
  • <name of interface>: This limits the type parameters to use the interface mentioned here, as shown with IEnumerable mentioned previously.
  • <name of class>: Any type used with this constraint must be of this type, or inherit from this type at some point in the inheritance chain.

Unfortunately, because numeric data structures are value types, they cannot inherit, and thus have no common type to use in a type constraint that will give you the mathematical operators needed to do math.

As a general rule of thumb, generics are most useful in "framework" style code, which is to say general infrastructure for your applications, or data structures such as collections. In fact, some great new collection types became available in C# 2.0.

Generic collections

Generics are perfect for collections because the collection itself doesn't really have to interact with the objects that it contains; it just needs a place to put them. So with a collection, there are no constraints on the type parameter. All of the new generic collections can be found in the namespace as follows:

using System.Collections.Generic;

As we discussed earlier, the most basic collection type that was in C# 1.0 was an ArrayList collection, which worked really well at the time. However, value types would be boxed as it used object as its payload type, and you had to cast the object out into your target object type every time you wanted to pull out a value. With generics, we now have List<T> as follows:

List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);

int value = list[1]; // returns 2;

The usage is practically identical to the ArrayList collection, but with the performance benefits of generics. Some of the other types available as generic classes are as follows:

  • Queue<T>: This is the same as the non-generic Queue, first in, first out (FIFO).
  • Stack<T>: There are no differences here from the non-generic version of the Stack, last in, first out (LIFO).
  • Dictionary<T, K>: This takes the place of the Hashtable collection from C# 1.0. It uses two generic parameters for the key and value of each dictionary item. This means that you can use a key other than a string.

Iterator methods

Perhaps one of the more unique features to arrive in C# 2.0 was that of iterator methods. They are a way of having the compiler automatically generate a custom iteration over a sequence. That description is kind of abstract, admittedly, so the easiest way to explain it is with some code, as follows:

private static IEnumerable<string> GetStates()
{
    yield return "Orlando";
    yield return "New York";
    yield return "Atlanta";
    yield return "Los Angeles";
}

In the previous method, you see a method that returns IEnumerable<string>. In the method body, however, there are simply four consecutive lines of code that use the yield keyword. This tells the compiler to generate a custom enumerator that breaks up the method into each individual part between the yields, so that it is executed when a caller enumerates the returned value. This is shown in the following code:

foreach (string state in GetStates())
{
    Console.WriteLine(state);
}
// outputs Orlando, New York, Atlanta, and Los Angeles

There are a lot of different ways to approach and use iterators, but the highlight here is how the C# compiler is getting smarter in this release. It is able to take your code and expand it. This lets you write code at a higher level of abstraction, which is an ongoing theme in the evolution of C#.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.40.189