Introduction to Generics in C#

Discussion in 'C#' started by Sanskruti, Apr 4, 2007.

  1. Sanskruti

    Sanskruti New Member

    Jan 7, 2007
    Likes Received:
    Trophy Points:
    Software Consultant
    Mumbai, India
    Generics are the most powerful feature of C# 2.0. Generics allow you to define type-safe data structures, without committing to actual data types. This results in a significant performance boost and higher quality code, because you get to reuse data processing algorithms without duplicating type-specific code. In concept, generics are similar to C++ templates, but are drastically different in implementation and capabilities.Generics permit classes, structs, interfaces, delegates, and methods to be parameterized by the types of data they store and manipulate.On the surface C# generics look syntactically similar to C++ templates, but there are important differences in the way they are implemented and supported by the compiler. Compared to C++ templates, C# generics can provide enhanced safety but are also somewhat limited in capabilities.

    In some C++ compilers, until you use a template class with a specific type, the compiler does not even compile the template code. When you do specify a type, the compiler inserts the code inline, replacing every occurrence of the generic type parameter with the specified type. In addition, every time you use a specific type, the compiler inserts the type-specific code, regardless of whether you have already specified that type for the template class somewhere else in the application.

    It is up to the C++ linker to resolve this, and it is not always possible to do. This may results in code bloating, increasing both the load time and the memory footprint. In .NET 2.0, generics have native support in IL (intermediate language) and the CLR itself. When you compile generic C# server-side code, the compiler compiles it into IL, just like any other type. However, the IL only contains parameters or place holders for the actual specific types. In addition, the metadata of the generic server contains generic information.

    The client-side compiler uses that generic metadata to support type safety. When the client provides a specific type instead of a generic type parameter, the client's compiler substitutes the generic type parameter in the server metadata with the specified type argument. This provides the client's compiler with type-specific definition of the server, as if generics were never involved. This way the client compiler can enforce correct method parameters, type-safety checks, and even type-specific IntelliSense.

    The interesting question is how does .NET compile the generic IL of the server to machine code. It turns out that the actual machine code produced depends on whether the specified types are value or reference type. If the client specifies a value type, then the JIT compiler replaces the generic type parameters in the IL with the specific value type, and compiles it to native code.

    However, the JIT compiler keeps track of type-specific server code it already generated. If the JIT compiler is asked to compile the generic server with a value type it has already compiled to machine code, it simply returns a reference to that server code. Because the JIT compiler uses the same value-type-specific server code in all further encounters, there is no code bloating. If the client specifies a reference type, then the JIT compiler replaces the generic parameters in the server IL with Object, and compiles it into native code. That code will be used in any further request for a reference type instead of a generic type parameter. Note that this way the JIT compiler only reuses actual code. Instances are still allocated according to their size off the managed heap, and there is no casting.

    Generics in .NET let you reuse code and the effort you put into implementing it.

    The types and internal data can change without causing code bloat, regardless of whether you are using value or reference types. You can develop, test, and deploy your code once, reuse it with any type, including future types, all with full compiler support and type safety. Because the generic code does not force the boxing and unboxing of value types, or the down casting of reference types, performance is greatly improved. With value types there is typically a 200 percent performance gain, and with reference types you can expect up to a 100 percent performance gain in accessing the type.
    To well know the use of generics lets examine the following code:
    //These are just few lines of code and not a program
    public class Stack
    	object[] items;
    	int count;
    	public void Push(object item) {...}
    	public object Pop() {...}
    We use the object type to store any type of data. The above simple Stack class stores its data in an object array, and its two methods, Push and Pop, use object to accept and return data. While the use of type object makes the Stack class very flexible. However, when a value is retrieved, the result of the Pop method must explicitly be cast back to the appropriate type like this:

    Stack stack = new Stack();
    stack.Push(new Customer());
    Customer c = (Customer)stack.Pop(); 
    If a value of a value type, such as an int, is passed to the Push method, it is automatically boxed. When the int is later retrieved, it must be unboxed with an explicit type cast:
    Stack stack = new Stack();
    int i = (int)stack.Pop(); 
    Such boxing and unboxing operations add performance overhead since they involve dynamic memory allocations and run-time type checks.

    A further issue with the Stack class is that it is not possible to enforce the kind of data placed on a stack. Indeed, a Customer instance can be pushed on a stack and then accidentally cast it to the wrong type after it is retrieved:
    Stack stack = new Stack();
    stack.Push(new Customer());
    string s = (string)stack.Pop(); 
    With generics let us see how  the code will be written
    public class Stack<T>
    	T[] items;
    	int count;
    	public void Push(T item) {...}
    	public T Pop() {...}
    When the generic class Stack<T> is used, the actual type to substitute for T is specified. In the following example, int is given as the type argument for T:
    Stack<int> stack = new Stack<int>();
    int x = stack.Pop(); 
    The Stack<int> type is called a constructed type. In the Stack<int> type, every occurrence of T is replaced with the type argument int. When an instance of Stack<int> is created, the native storage of the items array is an int[] rather than object[], providing substantial storage efficiency compared to the non-generic Stack. Likewise, the Push and Pop methods of a Stack<int> operate on int values, making it a compile-time error to push values of other types onto the stack, and eliminating the need to explicitly cast values back to their original type when they're retrieved.Generics provide strong typing, meaning for example that it is an error to push an int onto a stack of Customer objects. Just as a Stack<int> is restricted to operate only on int values, so is Stack<Customer> restricted to Customer objects, and the compiler will report errors on the last two lines in the following way:
    Stack<Customer> stack = new Stack<Customer>();
    stack.Push(new Customer());
    Customer c = stack.Pop();
    stack.Push(3); // Type mismatch error
    int x = stack.Pop(); // Type mismatch error 

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice