What s New in C 2.0?


What's New in C# 2.0?

After the previous short preview of the new ASP.NET version, I'd like to present to you the main features of the new C# version 2.0, which is the basis for the rest of the book. You may already know the new features from when Anders Hejlsberg introduced them at OOPSLA in Seattle in autumn 2002. Microsoft Research and the C# team have published some information, too. In comparison to the released news, some syntax changes have been made in the present version.

The new features are classified by four categories:

  • Generics

  • Iterators

  • Anonymous methods

  • Partial classes

By the way, multiple inheritance is unfortunately not one of the new features.

Generics

Generic classes (or Generics), functionally similar to templates in C++, aren't really a new invention of Microsoft, but of course are new in C#. The idea is to develop universal untyped classes and let the compiler automatically create the typed version of the class whenever it's required.

A small example will demonstrate a type of problem that is solved by the use of Generics. The following lines show the simplified definition of a list similar to the ArrayList class:

 public class MyList {    public void Add(object obj)    {       // ...    }    object this[int index]    {       get { /* ... */ }    } } 

Any untyped object can be added to the list and be read. While adding an untyped object is possible in a direct manner thanks to polymorphism, there has to be an explicit type conversion on a read access. The untyped declaration of the list results in two problems. The compiler has no chance to verify the content of the list and the necessary type conversions. Type failures will be recognized only at run time—or maybe never recognized at all.

You can solve both problems by using typed classes. In this particular case, you just need two specialized lists to accept instances of MyClass's respective integer values. The base class library provides an abstract base class, CollectionBase in the System.Collections namespace, that will enable you to create typed collections easily. You have to implement the body for the different methods and the indexer. Internally, the objects are stored in an untyped ArrayList, and calls are forwarded to this class. For reference types, this approach works very well, although a new class has to be explicitly developed for each data type. However, collections for value types created in this way are inefficient, because the data needs to be (un)boxed to be stored in the ArrayList internally.

The solution for problems like this is the use of generic classes. A blueprint of the class is created just once. Instead of using a particular data type or object, a specific placeholder is added. You'll find the syntax of a generic class in C# version 2.0 in Listing 1-1.

Listing 1-1: Creating a Generic Class in C#

start example
 public class MyList<ItemType> {    public void Add(ItemType obj)    {       // ...    }    public ItemType this[int index]    {       get { return ItemType.default; }    } } 
end example

A custom labeled data type is defined in angle brackets behind the class name according to the general guidelines—somewhat like a parameter. Within the scope of the class, the specified data type can be used without any restrictions, as if it really existed.

Note

Are you wondering what the default property used in Listing 1-1 is about? This virtual property returns the default value depending on the generic type used. For reference types, this is null; and for value types, this is whatever defined—for example, 0.

The desired data type has to be specified explicitly only on use. Here again, you'll need the angle brackets—for both the declaration of the variable and for the instantiation of the class (see Listing 1-2).

Listing 1-2: Using Generics in C#

start example
   MyList<MyClass> myList = new MyList<MyClass>();   MyClass myClass = new MyClass();   myList.Add(myClass);   MyClass myClass2 = myList[0]; 
end example

Using Generics, no type conversion is required when reading from the collection. Please note that the class is limited to the specified data type (and derived classes). But this is an advantage, because the compiler is now able to detect illegal assignments. This way the lines in Listing 1-3 will be marked as faulty at design time and not at run time.

Listing 1-3: Generics Type-Safe Capability

start example
   MyList<MyClass> myList = new MyList<MyClass>();   MyClass myClass = new MyClass();   myList.Add(myClass);   int myValue = myList[0]; // --> Compiler error 
end example

The code doesn't need any modifications to use the list template shown in the example with other types of data. If the collection has been implemented natively based on Generics, you may use value types without boxing and unboxing. In fact, Listing 1-4 shows a significant advantage in terms of performance compared to the integer list discussed earlier.

Listing 1-4: Generics Working with Value Types

start example
   MyList<int> myList = new MyList<int>();   myList.Add(3);   int myValue = myList[0]; 
end example

Internally, generic classes aren't a feature of the language-dependent compiler but of the Intermediate Language. In the context of Just-in-Time (JIT) compilation, the Intermediate Language expects to receive the desired data type at run time. In regard to value types, the run-time environment generates a specialized class for each type. Concerning int and long, two separate classes are created based on the same model. This is the only way in which the different types can be used in a native (and optimized) way. The usage of reference types, however, is different from the preceding description. Such types consist by definition of a uniform-sized pointer (a reference) on a memory block. Consequently, it isn't necessary to create internal type-optimized versions of the class. This is even better, because exactly the same class, created by the JIT compiler, can be used for all reference types. This way the compilation gets faster and consumes fewer resources.

More Options with the Power of Generics

The previously described list requires exactly one variable data type. Depending on the nature of the class, the supply of placeholders may vary. A practical example is a dictionary that can be typed individually on both the key and the value.

In this case, the new C# version allows multiple placeholders. They have to be specified in angle brackets, separated by commas. The handling of the generic class is the same as described previously, whereas Listing 1-5 shows clearly that the indexer and the returned value can be used in a typed way.

Listing 1-5: Using Generics to Allow More Than One Unknown Type

start example
 public class MyDictionary<KeyType, ValueType> {    public void Add(KeyType key, ValueType value)    {       // ...    }    public ValueType this[KeyType key]    {       get { return ValueType.default; }    } } 
end example

Although using Generics is type safe, you don't have type-safe access while developing the class itself. Because the type with which the generic class is used later is absolutely unknown, it's internally assumed to be object. Specific members of the data type can only be accessed after an explicit and therefore unsafe conversion. Possible failures will only be detected at run time. The example in Listing 1-6 shows that an exception will be thrown if the type passed to the KeyType placeholder doesn't support the IComparable interface.

Listing 1-6: Lack of Type Safety Internally for Generics

start example
 // MyList public class MyDictionary<KeyType, ValueType> {    public void Add(KeyType key, ValueType value)    {       switch(((IComparable) key).CompareTo(123))       {          case 0:             // ...             break;       }    } } 
end example

To resolve this problem, the placeholder types can be regularized with constraints. These have to be noted after the new where keyword, very similar to a SQL query (see Listing 1-7).

Listing 1-7: Generics Supporting Constraints

start example
 // MyList public class MyDictionary<KeyType, ValueType>    where KeyType : IComparable {    public void Add(KeyType key, ValueType value)    {       switch (((IComparable)key).CompareTo(123))       {          case 0:             // ...             break;       }    } } 
end example

You can also define several constraints for one as well as for several placeholder data types. The constraints have to be separated by a comma and are seen as additive, which means that all of them must comply to get acceptance from the compiler.

Generics are useful not only with classes. Structures, interfaces, and delegates as well can be declared as generic templates and be reused. On enumerations, however, Generics can't be applied. Methods are another exciting field of use for Generics. Generic methods will allow you to pass one or more data types. Listing 1-8 shows an example.

Listing 1-8: Using Generics with Methods

start example
 public class MyClass {    protected ItemType MyMethod<ItemType>(ItemType item)    {       return item;    } } 
end example

As you see, Generics are very important and useful if you work, for example, with any kind of collections. Because of the backwards compatibility of ASP.NET 2.0, the existing collections couldn't be modified. Instead, a new namespace named System.Collections.Generic was created. It contains a lot of generic classes, structures, and interfaces like the following:

  • Dictionary<T, U>

  • List<T>

  • Queue<T>

  • SortedDictionary<T, U>

  • Stack<T>

Iterators

The easiest way to walk through the subitems of a data container is a foreach loop, as you do with collections and dictionaries. But it's possible to walk through other classes, too. For example, the string class allows you to access the contained characters of type char in the same way.

A typical example for the enumeration of a class is described in Listing 1-9. It contains a foreach loop as well as the code that the compiler processes. Internally, a while loop is used that is based on the enumerator pattern.

Listing 1-9: foreach Loops Based on an Enumerator Pattern

start example
 // Implementation ArrayList list = new ArrayList(); // ... foreach(object obj in list) {    DoSomething(obj); } // Translation through compiler Enumerator e = list.GetEnumerator(); while(e.MoveNext()) {    object obj = e.Current;    DoSomething(obj); } 
end example

A pattern like the IEnumerable interface has to be implemented for the class. GetEnumerator() is the only defined method, and it has to return an object that supports the IEnumerator interface. The MoveNext() and Reset() methods as well as the Current property make it possible to iterate through the class.

It's the job of the new iterators in C# 2.0 to reduce the required implementation effort. No need to work with a design pattern; just one single typed method called GetEnumerator() is sufficient. Instead of an enumerator, the particular values will be returned directly. Here the new yield keyword replaces the old return keyword. (In Beta version 1, the keyword may possibly become yield return.) The keyword returns a value iteratively, the next value in the sequence being returned each time you pass through the foreach loop. Listing 1-10 illustrates the new approach.

Listing 1-10: Using Iterators to Iterate Through a Custom Class

start example
 using System; using System.Collections.Generic; public class Names {    public IEnumerator<string> GetEnumerator()    {       yield "Smith";       yield "Miller";       yield "Doe";    } } // Main public class MainClass {    public static void Main()    {       Names names = new Names();       foreach(string name in names)       {          Console.WriteLine(name);       }       Console.Read();    } } 
end example

You can easily guess what the sample does. The three listed names will appear within the console window. By the way, you can also use this keyword in loops as shown in Listing 1-11.

Listing 1-11: Using the New Yield Keyword in Loops

start example
 public class Names {    private List<string> names = new List<string>();    public Names()    {       this.names.Add("Smith");       this.names.Add("Miller");       this.names.Add("Doe");    }    public IEnumerator<string> GetEnumerator()    {       for(int i = 0; i < this.names.Count; i++)       {           yield this.names[i];       }    } } 
end example

Under the hood, almost everything remains unaffected. The C# compiler converts the new implementation into the well-known enumerator pattern. Actually, this kind of iteration is a convenience feature for the hard-pressed developer. It avoids unnecessary coding and keeps the source code short.

New and therefore absolutely necessary to mention is the capability to make any method enumerable. The method has to return a value defined as IEnumerable and use the yield keyword to return each single value. This will look somewhat like the following listing:

 public IEnumerable GetSomeValues(int start, int stop) {    for (int i = start; i <= stop; i++)    {        yield i;    } } ... foreach (int i in mc.GetSomeValues(0, 10)) {    Console.WriteLine(i); } 

Anonymous Methods

Anonymous methods are another improvement of the next generation of C#. They allow you to declare methods in the context of their use and without naming them.

Listing 1-12 demonstrates the use of anonymous methods within an ASP.NET page. During Page_Load an anonymous event-handling method is assigned to a Button control. The method will change the text of the Label control, which is also placed on the page.

Listing 1-12: Assigning and Handling Events Right Away

start example
 void Page_Load (object sender, System.EventArgs e) {    this.Button1.Click += delegate(object dlgSender, EventArgs dlgE)    {      Label1.Text = "Yeah, you clicked the button!";    }; } 
end example

What's new is that you can abandon an explicit notation of the event-handling method—as long as it's useful! Just put the code after the assignment of the delegate. Open a new scope after the delegate keyword by using a brace, enter the desired actions, and close the scope with another brace. Then you finish the whole statement with a semicolon.

Unlike what is being demonstrated in the previous example, you can use more than one line of code, of course. In doing so, you have access to the two common event parameters that have to be defined explicitly as you assign the delegate. Please notice that I've renamed these parameters to avoid a conflict with the ones for the Page_Load event.

Even if it looks somewhat unusual at first sight, it's possible to use the local variables of the outer scope within the anonymous method. According to this, the lines in Listing 1-13 are correct, although they aren't arranged very clearly.

Listing 1-13: Accessing Variables Declared in the Upper Scope

start example
 void Page_Load (object sender, System.EventArgs e) {    string text = "Yeah, you clicked the button!";    this.Button1.Click += delegate(object dlgSender, EventArgs dlgE)    {       Label1.Text = text;    }; } 
end example

Anonymous methods may be used with more than one event or even in a completely different way. If you want to, you can reference the delegate in a variable and handle it as usual, as Listing 1-14 shows.

Listing 1-14: Using the Delegate As Usual

start example
 void Page_Load (object sender, System.EventArgs e) {    EventHandler handler = delegate(object dlgSender, EventArgs dlgE)    {       Label1.Text = "Yeah, you clicked the button!";    };    this.Button1.Click += handler;    this.Button2.Click += handler; } 
end example

Internally, the C# compiler converts the anonymously implemented method into a class including a uniquely named method. The created delegate now points to this procedure. In real life, the usage of anonymous methods is particularly of interest with regard to smaller event-handling methods. For the sake of clarity, you should not overuse this method, particularly in combination with longer routines.

Another area you could apply such methods might be multithreading. Until now, an explicit method was required to start a new thread. In this particular case, the anonymous counterpart can provide even more clarity because the context becomes more obvious. In Listing 1-15, an approach based on a console application is exemplified. In this case, the dynamic method is directly passed in the constructor of the Thread class. This looks really funny, but it actually works!

Listing 1-15: The Entry Code for the Second Thread Passed As an Anonymous Method

start example
 using System; using System.Threading; namespace MyLittleConsoleApp {    public class MainClass    {       [STAThread]       static void Main()       {          Thread myThread = new Thread(delegate()          {             for(int i = 0; i < 20; i++)             {                Console.WriteLine("Working thread ...");                Thread.Sleep(500);             }          });          myThread.Start();          for (int i = 0; i < 10; i++)          {             Console.WriteLine("In main.");             Thread.Sleep(1000);          }          Console.Read();        }    } } 
end example

Partial Classes

The fourth main new feature of C# version 2.0 allows you to split a particular class into two or more separate files. For this purpose, every part of the class is marked with the new modifier partial. The compiler looks for the marked parts and merges them into one complete implementation. You'll see no difference at run time. The assembly of the code parts requires all elements be in the same project and a parallel compilation. In addition, the classes have to match logically and must be identical regarding their modifiers, supported interfaces, and so on. Attributes assigned at class level are handled automatically (see Listing 1-16).

Listing 1-16: One Class Split into Several Files Using the New Partial Keyword

start example
 // foo1.cs using System; public partial class Foo {    public void SomeMethod()    {    } } // foo2.cs using System; public partial class Foo {    public void SomeOtherMethod()    {    } } 
end example

The main reasons to implement such a feature can primarily be found in RAD development environments like Visual Studio .NET. OK, in some special cases it may be useful to split a large class into several files. But this surely won't be the general rule. One aspect you may find interesting is splitting files in conjunction with automatic code generation, as in Visual Studio .NET for example. Instead of mixing generated and written code and differentiating it later on, two separate files can now be used. More details about this topic will follow in Chapter 2.




ASP. NET 2.0 Revealed
ASP.NET 2.0 Revealed
ISBN: 1590593375
EAN: 2147483647
Year: 2005
Pages: 133

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net