Recipe4.2.Understanding Generic Types


Recipe 4.2. Understanding Generic Types

Problem

You need to understand how the .NET types work for generics and what differences there are from regular .NET types.

Solution

A couple of quick experiments can show the differences between regular .NET types and generic .NET types. When a regular .NET type is defined, it looks like the StandardClass type defined in Example 4-1.

Example 4-1. StandardClass: a regular .NET type

 public class StandardClass {     // Static counter hangs off of the Type for     // StandardClass.     static int _count = 0;     // Create an array of typed items.     int _maxItemCount;     object[] _items;     int _currentItem = 0;     // Constructor that increments static counter     public StandardClass(int items)     {         _count++;         _maxItemCount = items;         _items = new object[_maxItemCount];     }     /// <summary>     /// Add an item to the class whose type     /// is unknown as only object can hold any type.     /// </summary>     /// <param name="item">item to add</param>     /// <returns>the index of the item added</returns>     public int AddItem(object item)     {         if (_currentItem < _maxItemCount)         {             _items[_currentItem] = item;             return _currentItem++;         }         else             throw new Exception("Item queue is full");     }     /// <summary>     /// Get an item from the class.     /// </summary>     /// <param name="index">the index of the item to get</param>     /// <returns>an item of type object</returns>     public object GetItem(int index)     {         Debug.Assert(index < _maxItemCount);         if (index >= _maxItemCount)             throw new ArgumentOutOfRangeException("index");         return _items[index];     }     /// <summary>     /// The count of the items the class holds     /// </summary>     public int ItemCount     {         get { return _currentItem; }     }     /// <summary>     /// ToString override to provide class detail     /// </summary>     /// <returns>formatted string with class details</returns>     public override string ToString()     {         return "There are " + _count.ToString() +             " instances of " + this.GetType().ToString( ) +             " which contains " + _currentItem + " items of type " +             _items.GetType().ToString() + "…";     } } 

StandardClass has a static integer member variable, _count, which is incremented in the instance constructor, and a ToString( ) override that prints out how many instances of StandardClass exist in this appdomain. StandardClass also contains an array of objects(_items), the size of which is determined by the item count passed in to the constructor. It implements methods to add and retrieve items (AddItem, GetItem) and a read-only property to get the number of items in the array (ItemCount).

The GenericClass<T> type is a generic .NET type with the same static integer _count field, the instance constructor that counts the number of instantiations, and the overridden ToString( ) method to tell you how many instances there are of this type. GenericClass<T> also has an _items array, and methods corresponding to those in StandardClass, as you can see Example 4-2.

Example 4-2. GenericClass<T>: a generic .NET type

 public class GenericClass<T> {     // Static counter hangs off of the     // instantiated Type for     // GenericClass.     static int _count = 0;     // Create an array of typed items.     int _maxItemCount;     T[] _items;     int _currentItem = 0;     // Constructor that increments static counter     public GenericClass(int items)     {         _count++;         _ _maxItemCount = items;         _items = new T[_maxItemCount];     }     /// <summary>     /// Add an item to the class whose type.     /// is determined by the instantiating type.     /// </summary>     /// <param name="item">item to add</param>     /// <returns>the zero-based index of the item added</returns>     public int AddItem(T item)     {         if (_currentItem < _maxItemCount)         {             _items[_currentItem] = item;             return _currentItem++;         }         else             throw new Exception("Item queue is full");     }     /// <summary>     /// Get an item from the class.     /// </summary>     /// <param name="index">the zero-based index of the item to get</param>     /// <returns>an item of the instantiating type</returns>     public T GetItem(int index)     {         Debug.Assert(index < _maxItemCount);         if (index >= _maxItemCount)             throw new ArgumentOutOfRangeException("index");         return _items[index];     }     /// <summary>     /// The count of the items the class holds     /// </summary>     public int ItemCount     {         get { return _currentItem; }     }     /// <summary>     /// ToString override to provide class detail     /// </summary>     /// <returns>formatted string with class details</returns>     public override string ToString()     {         return "There are " + _count.ToString() +             " instances of " + this.GetType().ToString( ) +             " which contains " + _currentItem + " items of type " +                   _items.GetType().ToString() + "…";     } } 

Things start to get a little more different with GenericClass<T> when you look at the _items array implementation. The _items array is declared as:

  T [] _items; 

instead of:

 object [] _items; 

The _items array uses the type parameter of the generic class (<T>) to determine what type of items are allowed in the _items array. StandardClass uses object for the _items array type, which allows any type to be stored in the array of items (since all types derive from object), while GenericClass<T> provides type safety by allowing the type parameter to dictate what types of objects are permitted.

The next difference is visible in the method declarations of AddItem and GetItem. AddItem now takes a parameter of type T, whereas in StandardClass it took a parameter of type object. GetItem now returns a value of type T, whereas in StandardClass it returned a value of type object. These changes allow the methods in GenericClass<T> to use the instantiated type to store and retrieve the items in the array instead of having to allow any object to be stored as in StandardClass.

 /// <summary> /// Add an item to the class whose type /// is determined by the instantiating type. /// </summary> /// <param name="item">item to add</param> /// <returns>the zero-based index of the item added</returns> public int AddItem(T item) {     if (_currentItem < _maxItemCount)     {         _items[_currentItem] = item;         return _currentItem++;     }     else         throw new Exception("Item queue is full"); } /// <summary> /// Get an item from the class. /// </summary> /// <param name="index">the zero-based index of the item to get</param> /// <returns>an item of the instantiating type</returns> public T GetItem(int index) {     Debug.Assert(index < _maxItemCount);     if (index >= _maxItemCount)     throw new ArgumentOutOfRangeException("index");     return _items[index]; } 

There are a few advantages this provides. First and foremost is the type safety provided by GenericClass<T> for items in the array. It was possible to write code like this in StandardClass:

 // Regular class StandardClass C = new StandardClass(5); Console.WriteLine(C); string s1 = "s1"; string s2 = "s2"; string s3 = "s3"; int i1 = 1; // Add to the standard class (as object). C.AddItem(s1); C.AddItem(s2); C.AddItem(s3); // Add an int to the string array, perfectly OK. C.AddItem(i1); 

But GenericClass<T> will give a compiler error if you try the same thing:

 // Generic class GenericClass<string> gC = new GenericClass<string>(5); Console.WriteLine(gC); string s1 = "s1"; string s2 = "s2"; string s3 = "s3"; int i1 = 1; // Add to the generic class (as string). gC.AddItem(s1); gC.AddItem(s2); gC.AddItem(s3); // Try to add an int to the string instance, denied by compiler. // error CS1503: Argument '1': cannot convert from 'int' to 'string' //GC.AddItem(i1); 

Having the compiler prevent this before it can become the source of runtime bugs is a very good thing.

It may not be immediately noticeable, but the integer is actually boxed when it is added to the object array in StandardClass, as you can see in the IL for the call to GetItem on StandardClass:

 IL_0170: ldloc.2 IL_0171: ldloc.s i1  IL_0173: box [mscorlib]System.Int32 IL_0178: callvirt instance int32                     CSharpRecipes.Generics/StandardClass::AddItem(object) 

This boxing turns the int, which is a value type, into a reference type (object) for storage in the array. This causes extra work to be done to store value types in the object array

There is a problem when you go to get an item back from the class in the StandardClass implementation. Take a look at how StandardClass.GetItem retrieves an item:

 // Hold the retrieved string. string sHolder; // Have to cast or get error CS0266: // Cannot implicitly convert type 'object' to 'string'… sHolder = (string)C.GetItem(1); 

Since the item returned by StandardClass.GetItem is of type object, it needs to be cast to a string in order to get what you hope is a string for index 1. It may not be a stringall you know for sure is that it's an objectbut you have to cast it to a more specific type coming out so you can assign it properly. strings are a special case, since all objects can give a string representation of themselves, but you can see how this would be a problem if the array held a double and the assignment was to a bool.

These are both fixed by the GenericClass<T> implementation. The unboxing is addressed; no unboxing is required, since the return type of GetItem is the instantiated type, and the compiler enforces this by looking at the value being returned:

 // Hold the retrieved string. string sHolder; int iHolder; // No cast necessary sHolder = gC.GetItem(1); // Try to get a string into an int. // error CS0029: Cannot implicitly convert type 'string' to 'int' //iHolder = gC.GetItem(1); 

In order to see one other difference between the two types, instantiate a few instances of each of them like so:

 // Regular class StandardClass A = new StandardClass(); Console.WriteLine(A); StandardClass B = new StandardClass(); Console.WriteLine(B); StandardClass C = new StandardClass(); Console.WriteLine(C); // generic class GenericClass<bool> gA = new GenericClass<bool>(); Console.WriteLine(gA); GenericClass<int> gB = new GenericClass<int>(); Console.WriteLine(gB); GenericClass<string> gC = new GenericClass<string>(); Console.WriteLine(gC); GenericClass<string> gD = new GenericClass<string>(); Console.WriteLine(gD); 

The output from the preceding code shows this:

 There are 1 instances of CSharpRecipes.Generics+StandardClass which contains 0 items of type System.Object[]… There are 2 instances of CSharpRecipes.Generics+StandardClass which contains 0 items of type System.Object[]… There are 3 instances of CSharpRecipes.Generics+StandardClass which contains 0 items of type System.Object[]… There are 1 instances of CSharpRecipes.Generics+GenericClass`1[System.Boolean] which contains 0 items of type System.Boolean[]… There are 1 instances of CSharpRecipes.Generics+GenericClass`1[System.Int32] which contains 0 items of type System.Int32[]… There are 1 instances of CSharpRecipes.Generics+GenericClass`1[System.String] which contains 0 items of type System.String[]… There are 2 instances of CSharpRecipes.Generics+GenericClass`1[System.String] which contains 0 items of type System.String[]… 

Discussion

The type parameters in generics allow you to create type-safe code without knowing the final type you will be working with. In many instances you want the types to have certain characteristics, in which case you place constraints on the type (Recipe 4.12). Methods can also have generic type parameters when the class itself does not; Recipe 4.9 shows an example.

Notice that while StandardClass has three instances, GenericClass has one instance in which it was declared with <bool> as the type, one instance in which <int> was the type, and two instances in which <string> was the declaring type. This means that, while there is one .NET Type object created for each non-generic class, there is one .NET Type object for every type-specific instantiation of a generic class.

StandardClass has three instances in the example code because StandardClass has only one type that is maintained by the CLR. With generics, one type is maintained for each combination of the class template and the type arguments passed when constructing a type instance. To make it more clear, you get one .NET type for GenericClass<bool>, one .NET type for GenericClass<int>, and a third .NET type for GenericClass<string>.

The internal static _count member helps to illustrate this point, as static members of a class are actually connected to the type that the CLR hangs on to. The CLR creates any given only type once and then maintains it until the appdomain unloads. This is why the output from the calls to ToString() on these objects shows that the count is three for StandardClass (as there is truly only one of these) and between one and two for the GenericClass<T> types.

See Also

See the "Generic Type Parameters" and "Generic Classes" topics in the MSDN documentation.



C# Cookbook
Secure Programming Cookbook for C and C++: Recipes for Cryptography, Authentication, Input Validation & More
ISBN: 0596003943
EAN: 2147483647
Year: 2004
Pages: 424

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