Given the discussions in earlier chapters about the prevalence of objects within the CLI type system, it should come as no surprise to learn that generics are also objects. In fact, the type parameter on a generic class becomes metadata that the runtime uses to build appropriate classes when needed. Generics, therefore, support inheritance, polymorphism, and encapsulation. With generics, you can define methods, properties, fields, classes, interfaces, and delegates.
To achieve this, generics require support from the underlying runtime. In turn, the addition of generics to the C# language is a feature of both the compiler and the framework. To avoid boxing, for example, the implementation of generics is different for value-based type parameters than for generics with reference type parameters.
When a generic class is compiled, it is not significantly different from a nongeneric class. The result of the compilation consists of just metadata and CIL. The CIL is parameterized to accept a user-supplied type somewhere in code. As an example, suppose you had a simple Stack class declared as shown in Listing 12.46.
When you compile the class, the generated CIL is parameterized and looks something like Listing 12.47.
The first notable item is the '1 that appears following Stack on the second line. That number is the arity of the generic types: It declares the number of type parameters for which the generic class will require type arguments. A declaration such as EntityDictionary<TKey, TValue> would have an arity of 2.
The second line of the generated CIL shows the constraints imposed upon the class. The T type parameter is decorated with an interface declaration for the IComparable constraint.
If you continue looking through the CIL, you will find that the item’s array declaration of type T is altered to contain a type parameter using exclamation point notation, which is featured in the generics-capable version of the CIL. The exclamation point denotes the presence of the first type parameter specified for the class, as shown in Listing 12.48.
Beyond the inclusion of the arity and type parameter in the class header and the type parameter denoted with exclamation points in code, there is little difference between the CIL generated for a generic class and the CIL generated for a nongeneric class.
When a generic type is first constructed with a value type as a type parameter, the runtime creates a specialized generic type with the supplied type parameter(s) placed appropriately in the CIL. Therefore, the runtime creates new specialized generic types for each new parameter value type.
For example, suppose some code declared a Stack constructed of integers, as shown in Listing 12.49.
When using this type, Stack<int>, for the first time, the runtime generates a specialized version of the Stack class with the type argument int substituted for its type parameter. From then on, whenever the code uses a Stack<int>, the runtime reuses the generated specialized Stack<int> class. In Listing 12.50, you declare two instances of a Stack<int>, both using the code already generated by the runtime for a Stack<int>.
If, later in the code, you create another Stack with a different value type substituted for the type parameter (such as a long or a user-defined struct), the runtime will generate another version of the generic type. The benefit of specialized value type classes is better performance. Furthermore, the code can avoid conversions and boxing because each specialized generic class natively contains the value type.
Generics work slightly differently for reference types. The first time a generic type is constructed with a reference type, the runtime creates a specialized generic type with object references substituted for type parameters in the CIL, rather than a specialized generic type based on the type argument. Each subsequent time a constructed type is instantiated with a reference type parameter, the runtime reuses the previously generated version of the generic type, even if the reference type is different from the first reference type.
For example, suppose you have two reference types: a Customer class and an Order class. Next, you create an EntityDictionary of Customer types:
EntityDictionary<Guid, Customer> customers;
Prior to accessing this class, the runtime generates a specialized version of the EntityDictionary class that, instead of storing Customer as the specified data type, stores object references. Suppose the next line of code creates an EntityDictionary of another reference type, called Order:
EntityDictionary<Guid, Order> orders =
new EntityDictionary<Guid, Order>();
Unlike with value types, no new specialized version of the EntityDictionary class is created for the EntityDictionary that uses the Order type. Instead, an instance of the version of EntityDictionary that uses object references is instantiated and the orders variable is set to reference it.
To still gain the advantage of type safety, for each object reference substituted in place of the type parameter, an area of memory for an Order type is specifically allocated and the pointer is set to that memory reference. Suppose you then encountered a line of code to instantiate an EntityDictionary of a Customer type as follows:
customers = new EntityDictionary<Guid, Customer>();
As with the previous use of the EntityDictionary class created with the Order type, another instance of the specialized EntityDictionary class (the one based on object references) is instantiated and the pointers contained therein are set to reference a Customer type specifically. This implementation of generics greatly reduces code bloat by ensuring that the compiler creates only one specialized class for generic classes of reference types.
Even though the runtime uses the same internal generic type definition when the type parameter on a generic reference type varies, this behavior is superseded if the type parameter is a value type. Dictionary<int, Customer>, Dictionary<Guid, Order>, and Dictionary<long, Order> will require new internal type definitions, for example.
The implementation of generics in Java occurs entirely within the compiler, not within the Java Virtual Machine. Sun Microsystems, which originally developed Java (long before Oracle took it over), adopted this approach to ensure that no updated Java Virtual Machine would need to be distributed because generics were used.
The Java implementation uses syntax like the templates in C++ and the generics in C#, including type parameters and constraints. Because it does not treat value types differently from reference types, however, the unmodified Java Virtual Machine cannot support generics for value types. As such, generics in Java do not offer the same gains in execution efficiency as they do in C#. Indeed, whenever the Java compiler needs to return data, it injects automatic downcasts from the specified constraint, if one is declared, or the base Object type, if a constraint is not declared. Further, the Java compiler generates a single specialized type at compile time, which it then uses to instantiate any constructed type. Finally, because the Java Virtual Machine does not support generics natively, there is no way to ascertain the type parameter for an instance of a generic type at execution time, and other uses of reflection are severely limited.