Generics : C# Advanced features - Part 1
This article gives an introduction to a new feature introduced into C# Language called "Generics". The concept is explained with the help of several samples.
In the ”VS for Yukon” version of the C# language, Microsoft plans to build on an already elegant and expressive syntax by incorporating a variety of features across a broad spectrum of research and industry languages. Included among these language features are
Generics
Iterators
Anonymous methods
Partial classes or Partial types
Generics
C# will include a type-safe, high-performance version of generics which differs slightly in syntax and greatly in implementation from templates as found in C++ and generics as proposed for the Java language.
Generics are used to help make the code in the software components much more reusable.They are a type of data structure that contains code that remains the same.
Generics permit classes, structs, interfaces, delegates, and methods to be parameterized by the types of data they store and manipulate.
generics are declared and type checked at compile time while instantiated at runtime just like any other object.
Generic classes
programmers can create a limited version of true generic types by storing data in instances of the base object type. Since every object in C# inherits from the base object type and because of the boxing and unboxing features of the unified .NET type system, programmers can store both reference and value types into a variable whose type is object. However, there are performance penalties for converting between reference types and value types and the base object type.
To illustrate, let’s create a simple Stack type with two actions, “Push” and “Pop”. The Stack class stores its data in an array of object types, and the Push and Pop methods use the base object type to accept and return data, respectively:
public class Stack
{
private object[] items = new object[100];
public void Push(object data)
{
...
}
public object Pop()
{
...
}
}
We can then push our own custom type, for example a Customer type, onto the stack. However, if we wanted to retrieve the data, we would need to explicitly cast the result of the Pop method, a base object type, into a Customer type:
Stack s = new Stack();
s.Push(new Customer());
Customer c = (Customer) s.Pop();
If we pass a value type, such as an integer, to the Push method, the runtime will automatically convert it into a reference type, a process known as boxing, and then store it in the internal data structure. Similarly, if we wanted to retrieve a value type, such as an integer, from the stack, we would need to explicitly cast the object type we obtain from the Pop method into a value type, a process known as unboxing:
Stack s = new Stack();
s.Push(3);
int i = (int) s.Pop();
The boxing and unboxing operations between value and reference types can be particularly onerous.
Furthermore, in our current implementation, it is not possible to enforce the kind of data placed in the stack. Indeed, we could create a stack and push a Customer type onto it. Later, we could use the same stack and try to pop data off of it and cast it into a different type, as in the following example:
Stack s = new Stack();
s.Push(new Customer());
Employee e = (Employee) s.Pop();
However, while the code above is an improper use of the single type Stack class we want to implement and should be an error, it is actually legal code and the compiler will not have a problem with it. At run-time, however, the application will fail because we have performed an invalid cast operation.
Creating and consuming generics:
generics in C# are declared in much the same way as they are in C++. Programmers can create classes and structures just as they normally have, and by using the angle bracket notation (< and >) they can specify type parameters. When the class is used, each parameter must be replaced by an actual type that the end user of the class supplies.
In the example below, we can create a Stack class where we specify a type parameter, called ItemType, declared in angle brackets after the class declaration. Rather than forcing conversions to and from the base object type, instances of the generic Stack class will accept the type for which they are created and store data of that type natively. The type parameter ItemType acts as a proxy until that type is specified during instantiation and is used as the type for the internal items array, the type for the parameter to the Push method, and the return type for the Pop method:
public class Stack
{
private ItemType[] items;
public void Push(ItemType data)
{
...
}
public ItemType Pop()
{
...
}
}
When we use the Stack class, as in the short example below, we can specify the actual type to be used by the generic class. In this case, we instruct the Stack class to use a primitive integer type by specifying it as a parameter using the angle notation in the instantiation statement:
Stack stack = new Stack;
stack.Push(3);
int x = stack.Pop();
If we wanted to store items other than an integer into a Stack class, we would have to create a new instance of the Stack class, specifying the new type as the parameter. Suppose we had a simple Customer type and we wanted to use a Stack object to store it. To do so, we simply instantiate the Stack class with the Customer object as the type parameter and easily reuse our code:
Stack stack = new Stack;
stack.Push(new Customer());
Customer c = stack.Pop();
Of course, once we’ve created a Stack class with a Customer type as its parameter, we are now limited to storing only Customer types in the stack. Indeed, generics in C# are strongly typed, meaning we can no longer improperly store an integer into the stack, like so:
Stack stack = new Stack;
stack.Push(new Customer());
stack.Push(3) // compile-time error
Customer c = stack.Pop(); // no cast required.
Generics in the runtime:
When a generic class is compiled, there is actually nothing different between it and a regular class. Indeed, the result of the compilation is nothing but metadata and Intermediate Language (IL). The IL is, of course, parameterized to accept a user-supplied type somewhere in code. How the IL for a generic type is used differs based on whether or not the supplied type parameter is a value or reference type.
When a generic type is first constructed with a value type as a parameter, the runtime will create a specialized generic type with the supplied parameter (or parameters) substituted in the appropriate places in the IL. Specialized generic types are created once for each unique value type used as a parameter.
For example, suppose our code declared a Stack constructed of integers, like so:
Stack[int] stack;
At this point, the runtime will generate a specialized version of the Stack class with the int substituted appropriately for its parameter. Now, whenever our code uses a Stack of integers, the runtime will reuse the generated specialized Stack class.
Generics work slightly differently for reference types. The first time a generic type is constructed with any reference type, the runtime will create a specialized generic type with object references substituted for the parameters in the IL. Then, each time a constructed type is instantiated with a reference type as its parameter, regardless of what type it is, the runtime will reuse the previously created specialized version of the generic type.
Advantages:
Generics allow us to author, test, and deploy our code once, and reuse that code for a variety of different data types.
The Program becomes statically typed, so errors are discovered at compile-time.
No runtime casts are required and the program runs faster.
Primitive type values (e.g int) need not be wrapped. Hence the program is faster and uses less space.