Wrox Home  
Search
Professional C# 2005
by Christian Nagel, Bill Evjen, Jay Glynn, Karli Watson, Morgan Skinner, Allen Jones
November 2005, Paperback


Excerpt from Professional C# 2005

Generics

One of the biggest changes of the C# language and the CLR is the introduction of generics. With .NET 1.0, creating a flexible class or method that should use classes that are not known at compile time must be based on the Object class. With the Object class, there's no type safety during compile time. Casting is necessary. Also, using the Object class for value types has a performance impact.

.NET 2.0 supports generics. With generics the Object class is no longer necessary in such scenarios. Generic classes make use of generic types that are replaced with specific types as needed. This allows for type safety: the compiler complains if a specific type is not supported with the generic class.

Generics are a great feature, especially with collection classes. Most of the .NET 1.0 collection classes are based on the Object type. .NET 2.0 offers new collection classes that are implemented as generics.

Generics are not limited to classes; you also see generics with delegates, interfaces, and methods.

Overview

Generics are not a completely new construct; similar concepts exist with other languages. For example, C++ templates can be compared to generics. However, there's a big difference between C++ templates and .NET generics. With C++ templates the source code of the template is required when a template is instantiated with a specific type. Contrary to C++ templates, generics are not only a construct of the C# language; generics are defined with the CLR. This makes it possible to instantiate generics with a specific type in Visual Basic even though the generic class was defined with C#.

The following sections look into the advantages and disadvantages of generics.

Performance

One of the big advantages of generics is performance. In Chapter 9, collection classes from the namespace System.Collections were used to work with multiple objects. Adding a value type to such a collection results in boxing and unboxing when the value type is converted to a reference type and vice versa.

Here is a short look at boxing and unboxing.

Value types are stored on the stack. Reference types are stored on the heap. C# classes are reference type; structs are value types. .NET makes it easy to convert value types to reference types, and so you can use a value type everywhere an object (which is a reference type) is needed. For example, an int can be assigned to an object. The conversion from a value type to a reference type is known as boxing. Boxing happens automatically if a method requires an object as a parameter, and a value type is passed. On the other side, a boxed value type can be converted to a value type using unboxing. With unboxing the cast operator is required.

The following example shows the ArrayList class from the namespace System.Collections. ArrayList stores objects, the Add() method is defined to require an object as a parameter, and so an integer type is boxed. When the values from an ArrayList are read, unboxing occurs when the object is converted to an integer type. This may be obvious with the cast operator that is used to assign the first element of the ArrayList collection to the variable i1, but also happens inside the foreach statement where the variable i2 of type int is accessed:

ArrayList list = new ArrayList();
list.Add(44);   
	// boxing - convert a value type to a reference type

int i1 = (int)list[0];   
	// unboxing - convert a reference type to a value type

foreach (int i2 in list)
{
   Console.WriteLine(i2);   // unboxing
}

Boxing and unboxing is easy to use, but it has a big performance impact, especially when iterating through many items.

Instead of using objects, the List<T> class from the namespace System.Collections.Generic allows you to define the type when it is used. In the example here, the generic type of the List<T> class is defined as int, and so the int type is used inside the class that is generated dynamically from the JIT compiler. Boxing and unboxing no longer happens:

List<int> list = new List<int>();
list.Add(44);   
	// no boxing - value types are stored in the List<int>

int i1 = list[0];   // no unboxing, no cast needed

foreach (int i2 in list)
{
   Console.WriteLine(i2);
}

Type Safety

Another feature of generics is type safety. As with the ArrayList class, if objects are used, any type can be added to this collection. This example shows adding an integer, a string, and an object of type MyClass to the collection of type ArrayList:

ArrayList list = new ArrayList();
list.Add(44);
list.Add("mystring");
list.Add(new MyClass());

Now if this collection is iterated using the following foreach statement which iterates using integer elements, the compiler accepts this code. However, because not all elements in the collection can be cast to an int, a runtime exception will occur:

foreach (int i in list)
{
   Console.WriteLine(i);
}

Errors should be detected as early as possible. With the generic class List<T>, the generic type T defines what types are allowed. With a definition of List<int>, only integer types can be added to the collection. The compiler doesn't compile this code because the Add() method has invalid arguments:

List<int> list = new List<int>();
list.Add(44);
list.Add("mystring");   // compile time error
list.Add(new MyClass());   // compile time error

Binary Code Reuse

Generics allow better binary code reuse. A generic class can be defined once and can be instantiated with many different types. You needn't access the source code as is necessary with C++ templates.

As an example, here the List<T> class from the namespace System.Collections.Generic in the assembly mscorlib is instantiated with an int, a string, and a MyClass type:

List<int> list = new List<int>();
list.Add(44);

List<string> stringList = new List<string>();
stringList.Add("mystring");

List<MyClass> myclassList = new List<MyClass>();
myClassList.Add(new MyClass());

Generic types can be defined in one language and used from any other .NET language.

Code Bloat

How much code is created with generics when instantiating them with different specific types?

Because a generic class definition goes into the assembly, instantiating generic classes with specific types doesn't duplicate these classes in the IL code. However, when the generic classes are compiled by the JIT compiler to native code, a new class for every specific value type is created. Reference types share all the same implementation of the same native class. This is because with reference types only a 4-byte memory address (with 32-bit systems) is needed within the generic instantiated class to reference a reference type. Value types are contained within the memory of the generic instantiated class, and because every value type can have different memory requirements, a new class for every value type is instantiated.

Naming Guidelines

If generics are used in the program, it helps when generic types can be distinguished from non-generic types. Here are naming guidelines for generic types:

  • Generic type names should be prefixed with the letter T.
  • If the generic type can be replaced by any class because there's no special requirement, and only one generic type is used, the character T is good as a generic type name:
public class List<T> { }

public class LinkedList<T> { }
  • If there's a special requirement for a generic type (for example, it must implement an interface or derive from a base class), or if two or more generic types are used, descriptive names should be used for the type names:
public delegate 
	void EventHandler<TEventArgs> (object sender, TEventArgs e);

public delegate TOutput Converter<TInput, TOutput>(TInput from);

public class SortedList<TKey, TValue> { }

Generic Collection Classes

Many generic interfaces and collection classes are defined in the namespace System.Collections.Generic. These classes can be used instead of the collection classes that were discussed in Chapter 9, "Collections." Many of the classes dealt with in the previous chapter have a similar generic class, so this section does not differentiate between a list and a dictionary but focuses on generic features instead.

Generic Collections Overview

Before taking a closer look at how to use the collection classes, this section gives you an overview of which generic collection classes and interfaces are available.

The major interfaces and their functionality are defined in the following table.

Interface Methods and Properties Description
ICollection<T> Add(), Clear(), Contains(), CopyTo(), Remove()Count, IsReadOnly The interface ICollection<T> is implemented by collection classes. Methods of this interface can be used to add and remove elements from the collection.The generic interface ICollection<T> inherits from the non-generic interface IEnumerable. With this it is possible to pass objects implementing ICollection<T> to methods that require IEnumerable objects as parameters.
IList<T> Insert(), RemoveAt(), IndexOf()Item The interface IList<T> allows you to access a collection using an indexer. It is also possible to insert or remove elements at any position of the collection.Similar to ICollection<T>, the interface IList<T> inherits from IEnumerable.
IEnumerable<T> GetEnumerator() The interface IEnumerable<T> is required if a foreach statement is used with the collection. This interface defines the method GetEnumerator() that returns an enumerator implementing IEnumerator<T>.The generic interface IEnumerable<T> inherits from the non-generic interface IEnumerable.
IEnumerator<T> Current The foreach statement uses an enumerator implementing IEnumerator<T> for accessing all elements in a collection. The interface IEnumerator<T> inherits from the non-generic interfaces IEnumerator and IDisposable. The interface IEnumerator defines the methods MoveNext() and Reset(), IEnumerator<T> defines the type-safe version of the property Current.
IDictionary<TKey, TValue> Add(), ContainsKey(), Remove(), TryGetValue(),Item, Keys, Values The interface IDictionary<K, V> is implemented by collections whose elements have a key and a value.
IComparer<T> Compare() The interface IComparer<T> is used to sort elements inside a collection with the Compare() method.
IEqualityComparer<T> Equals(), GetHashCode() IEqualityComparer<T> is the second interface to compare objects. With this interface the objects can be compared for equality. The method GetHashCode() should return a unique value for every object. The method Equals() returns true if the objects are equal, false otherwise.

The generic collection classes and their functionality are shown in the following table.

Class Implemented Interfaces Description
List<T> IList<T>
ICollection<T>
IEnumerable<T>
The List<T> class is the generic replacement for the ArrayList class. Similarly to the ArrayList class, the List<T> class can grow and shrink dynamically. Besides the implementation of the three implemented interfaces, this class also supports additional functionality like sorting and reversing the elements in the collection.
Dictionary<TKey, TValue> IDictionary<TKey, TValue>
ICollection<KeyValuePair<TKey, TValue>>
IEnumerable<KeyValuePair<TKey, TValue>>
ISerializableIDeserializationCallback
Dictionary&l;tTKey, TValue> is a collection class that stores key and value pairs.
SortedList<TKey, TValue> IDictionary<TKey, TValue>
ICollection<KeyValuePair<TKey, TValue>>
IEnumerable<KeyValuePair<TKey, TValue>>
SortedList<TKey, TValue> is similar to Dictionary<TKey, TValue> with the difference that this collection is sorted by the key.
LinkedList<T> ICollection<T>
IEnumerable<T>
ISerializableIDeserializationCallback
LinkedList<T> is a double-linked list. With a double-linked list one element references the next and the previous.
Queue<T> ICollection<T>
IEnumerable<T>
Queue<T> is a collection with first-in, first-out behavior. The element that was added first will be read first from the queue. This is similar to a print-queue where the oldest print jobs are processed first. The method Enque() adds an object to the end of the queue; the method Deque() returns and removes an object from the beginning of the queue. With the method Peek() it is possible to read the first object from the queue without removing it.
Stack<T> ICollection<T>
IEnumerable<T>
Stack<T> is a collection with first-in, last-out behavior. The element that was added last will be read first from the stack. The Stack<T> offers the methods Push() and Pop(). With Push() an object is added to the end of the stack, while Pop() reads and removes the object from the end of the stack. The method Peek() reads the last object from the stack without removing it.

The .NET Framework offers some generic collection base classes that can be used to create a custom generic collection class. The classes in the following table are found in the namespace System.Collections.ObjectModel.

Class Implemented Interfaces Description
Collection<T> IList<T>
ICollection<T>
IEnumerable<T>
The class Collection<T> can be used as a base class for a custom generic collection class. It implements the interfaces IList<T>, ICollection<T>, and IEnumerable<T>. Behind the scenes, this class makes use of the List<T> class. However, it doesn't offer all the methods that List<T> does. So with a custom generic collection class you can offer the methods that are appropriate for the use case.With a custom collection class you can access the underlying List<T> object with the protected property Items.
ReadOnlyCollection<T> IList<T>
ICollection<T>
IEnumerable<T>
ReadOnlyCollection<T> is a read-only version of the class Collection<T>. All the methods from the interfaces that allow adding and removing elements are explicitly implemented and throw a NotSupportedException. An object of this class can be created by passing any collection that implements the IList<T> interface to the constructor.
KeyedCollection<TKey, TItem> IList<TItem>
ICollection<TItem>
IEnumerable<TItem>
KeyedCollection<TKey, TItem> is an abstract base class that can be used for custom collection classes that have a key and a value. This class inherits from Collection<TItem>.

Using the List<T> Class

The class List<T> in the namespace System.Collections.Generic is a very similar in its usage to the ArrayList class from the namespace System.Collections. This class implements the IList, ICollection, and IEnumerable interfaces. Because Chapter 9 already discussed the methods of these interfaces, this section looks at how to use the List<T> class.

The following example uses the class Racer as elements to be added to the collection to represent a Formula-1 racer. This class has two fields, a name and a car, that can be accessed with properties. With the constructor of the class the name of the racer and the car can be passed to set the members. The method ToString() is overridden to return the name of the racer and the car:

 public class Racer
   {
      private string name;
      public string Name
      {
         get
         {
            return name;
         }
      }

      private string car;
      public string Car
      {
         get
         {
            return car;
         }
      }

      public Racer(string name, string car)
      {
         this.name = name;
         this.car = car;
      }

      public override string ToString()
      {
         return name + ", " + car;
      }
   }

The variable racers is defined to be of type List<Racer>. With the new operator a new object of the same type is created. Here the default constructor of the List<T> class is used. This class also has a constructor to reserve memory for a specific number of items, and a constructor to copy elements from another collection of type List<T>.

Because the class List<T> was instantiated with the concrete class Racer, now only Racer objects can be added with the Add() method. With the class List<T>, the Add() method is defined as void Add(T item). In the following sample code five Formula-1 racers are created and added to the collection. Then, using the foreach statement, every team in the collection is iterated to be displayed on the console:

 List<Racer> racers = new List<Racer>();
 racers.Add(new Racer("Michael Schumacher", "Ferrari"));
 racers.Add(new Racer("Juan Pablo Montoya", "McLaren-Mercedes"));
 racers.Add(new Racer("Kimi Raikkonen", "McLaren-Mercedes"));
 racers.Add(new Racer("Mark Webber", "Williams-BMW"));
 racers.Add(new Racer("Rubens Barichello", "Ferrari"));

 foreach (Racer r in racers)
 {
    Console.WriteLine(r);
 }

With the List<T> class, not only can you add items and access them by using an enumerator; this class also has methods to insert and remove elements, clear the collection, and copy the elements to an array. Even more powerful functionality is discussed next. List<T> offers methods to search and to convert elements, reverse the order, and so on.

Finding elements

In this section, you search for all racers in the collection that drive a Ferrari.

The List<T> class offers Find() and FindAll() methods with these declarations:

public T Find(Predicate<T> match);
public List<T> FindAll(Predicate<T> match);

Both methods require a Predicate<T> as an argument. Predicate<T> is a delegate that references a predicate method. A predicate is a method that returns a Boolean value. If the predicate returns true, there's a match and the element is found. If it returns false, the element is not added to the result of the search. With the definition of Predicate<T>, the predicate must have a single argument of type T. The Find() method returns the first element with a predicate match, and the FindAll() method returns all elements with a predicate match in a list collection.

So you have to define a predicate. You use the predicate method DrivingCarPredicate() for finding a racer with a specific car. This method is defined in the class FindRacer, which can be initialized with the car to search. DrivingCarPredicate() receives a Racer object and compares the car of the Racer object with the car that is set in the constructor to return true or false accordingly:

  public class FindRacer
  {
     private string car;
     public FindRacer(string car)
     {
        this.car = car;
     }

     public bool DrivingCarPredicate(Racer racer)
     {
        return racer.Car == car;
     }
  }

To find the specific racers, the FindRacer class is initiated and initialized with a Ferrari, because you want to find all Ferrari racers. With the FindAll() method of the List<T> class, a new predicate delegate is instantiated, and this predicate delegate receives the finder.DrivingCarPredicate method. FindAll() returns a list of type List<Racer> that is used with the foreach to iterate all returned racers to display them on the console:

 FindRacer finder = new FindRacer("Ferrari");

 foreach (Racer racer in 
       racers.FindAll(new Predicate<Racer>(finder.DrivingCarPredicate)))
 {
    Console.WriteLine(racer);
 }

As a result, Michael Schumacher and Rubens Barichello are displayed on the console.