Click here to Skip to main content
15,879,613 members
Articles / Programming Languages / C#

.NET Generics in a Nutshell

Rate me:
Please Sign up or sign in to vote.
4.33/5 (8 votes)
23 Dec 2009CPOL10 min read 49.4K   47   8
Generics in .NET explained.

Inspiration Behind this Article

Ever since Generics was introduced with .NET 2.0, I have been searching the web for a good article about this important feature, but except for a few, I could not find a single article where I could get most of the things about Generics. So, I thought of writing an article on this where I will try and assemble all the important features/characteristics of Generics into a single page so beginners don't have to search further.

Introduction

Generics is one of the most important features that was introduced as part of .NET 2.0. It is syntactically similar to C++ templates, but differ in the implementation and feature list, and above all, it assures type safety and provides intellisense support for programmers. They also differ in characteristics and implementation. Generics help us define type safe user defined data types with out committing about the actual internal data types. This help us in a significant performance boost and at the same time will make the code more reusable. Let us consider a problem statement and analyse how Generics resolves the issue.

Problem statement: To implement a linked list kind of data structure that has two data members, and the type of these two elements can vary as per the given scenario. To create this type of data structure, we can proceed as follows:

C#
public class Node
{
    public int item;
    public int key;
    public Node NextNode;
    public Node()
    {
        item=0;
        key=0;    
        NextNode=null;
    }
    public Node(int a, int b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

Consider a LinkedList data structure that uses this Node:

C#
public class LinkedList
{
    Node head;
    public LinkedList()
    {
        head=new Node(1,1,null);
    }
    public void AddNode(int item,int Key)
    {
        Node newNode=new Node(item,Key,head.NextNode);
        head.NextNode=newnode;
    }
}

Having written this type of data structure, it is evident that we can store only data of type integer in the linked list. In order to store data of string type, we have to create a new linked list from scratch. The Node class of that LinkedList will look like:

C#
public class Node
{
    public string item;
    public int key;
    public Node NextNode;
    public Node()
    {
        item="";
        key=0;    
        NextNode=null;
    }
    public Node(string a, int b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

Or the other solution would be, instead of hard coding the type of data members of the Node class, we can put them as objects and then use the object type everywhere. This then will look like:

C#
public class Node
{
    public object item;
    public object key;
    public Node NextNode;
    public Node()
    {
        item=null;
        key=null;    
        NextNode=null;
    }
    public Node(object a, object b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

Consider a LinkedList data structure that uses this Node:

C#
public class LinkedList
{
    Node head;
    public LinkedList()
    {
        head=new Node(1,1,null);
    }
    public void AddNode(object item,object Key)
    {
        Node newNode=new Node(item,Key,head.NextNode);
        head.NextNode=newnode;
    }
    public object this[object key]
       {
       get{return Find(key);}
      }
    public object Find(object k)
    {
        //...search and comparison goes here
    }
}

Having created this, it would definitely relieve us from writing code for the LinkedList from scratch each time we want data of a different type to be stored, but at the same time, it will bring some disadvantages with itself.

  • Performance reduction due to the overhead of boxing and unboxing.
  • Type safety issue.

Let us take a look at this and discuss the solutions.

Consider the following code:

C#
LinkedList s=new LinkedList();
s.AddNode(12,13);
s.AddNode("Abc",15);

At present, there are three nodes in the list. Let us try to find an item based on the key. For that, I write this code:

C#
int n;
n=(int)s[13];

Casting an object into int would reduce performance. At the same time:

C#
int n;
n=(int)s[15];

This is not type safe because if the item stored at a particular key is a string that does not hold a digit, then it would give us a runtime error.

Enter Generics

Generics is a technique that allows us to define type safe user defined types without compromising performance or productivity. We can define the LinkedList once and can use type combination on a later stage depending on our requirements. Let us understand Generics now. Generics in C# is syntactically similar to C++ templates, but differ in how they are handled by the compiler. In C++, the compiler does not even compile the generic code till the time the actual types are specified. When the actual types are specified, the compiler inserts the type specific information inline and then compiles the code to machine code. In contrast to C++, in C#, the compiler compiles the generic code into IL and places the placeholders wherever it finds a generic type T. Let us define the above LinkedList using Generics.

C#
public class Node<T,K>
{
    public T item;
    public K key;
    public Node NextNode;
    public Node()
    {
        item=default(T);
        key=default(K);    
        NextNode=null;
    }
    public Node(T a, K b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

Consider a LinkedList data structure that uses this Node:

C#
public class LinkedList<T,K>
{
    Node<int,string> head;
    public LinkedList()
    {
        head=new Node<int,string>(1,"1",null);
    }
    public void AddNode(T item,K Key)
    {
        Node<int,string> newNode=new Node<int, string>(item, 
                                               Key, head.NextNode);
        head.NextNode=newnode;
    }
    public T this[K key]
       {
       get{return Find(key);}
      }
    public  T Find(K k)
    {
        //...search and comparison goes here
    }
}

Prior to Generics, we would create an object of the LinkedList class the way we wanted, and call the AddNode method with any type of parameters. That would have created issues like type casting and type safety as mentioned above, but with Generics, here is the way we declare an object of a generic class LinkedList:

C#
LinkedList<int,string> s=new LinkedList<int,string>();

By doing this, we have almost the same type of functionality that we gained from having object type members, but here we get type safety. We force the compiler to take item as int and key as string type. Let us try and make some calls to the AddNode() and Find() methods and find out how it differs from the earlier one.

C#
//The complier will not compile, but in earlier case it would have compiled.
s.AddNode(12,1);
s.AddNode(12,"1");// Would compile and run.

Now, let as call the Find method to get the value stored in a particular node with its key value.

C#
string item1;
tem1=s["1"];
// this would not compile as the compiler will give
// cast mismatch error. So programmer is warned at the compile time itself.
// but in earlier case it would have compiled and raised runtime exception,
// and also there is no need of casting.

Generic Constraints

Let us define the Find() method first:

C#
public T Find(K k)
{
    Node<T,K> current=head;
    while(current.NextNode!=null)
    {
        if(current.Key==k) //Will not compile
            break;
        else
            current=current.NextNode;
    }
    return current.Item;
}

The C# compiler compiles the code into IL, depending upon the type being used by the client. There can be case that the generic fields may try to implement methods, properties, or operators which are otherwise incompatible with the specific type. Consider the Find() method above and the line if (current.Key == k). This would not compile because the compiler can not judge whether K or the actual type specified to K will support the == operator for equality check. For example, structs don't allows this operator to be used for equality check. To overcome this issue, we could use the CompareTo() method like follows:

C#
if(current.Key.CompareTo(k)==0)

But the issue still persists as this time also, the code will not compile as the compiler will again fail to judge whether K or the actual type is derived from IComparable. To overcome such issues, in C#, we need to instruct the compiler which constraints the client-specified types must obey in order for them to be used instead of the generic type parameters. We force these restrictions with the help of Contraints. In C#, there are three types of Constraints, but can be applied in five different ways. Let us discuss them one by one.

1. Derivative Contraints

1.1 Interface Derivation

Derivative constraints direct the compiler that the generic parameter derives from an interface or class. To implement this, we use the where reserved word of C# as follows:

C#
public class LinkedList<T,K> where K:IComparable
{
}

Having written this type of definition for the class, we force the calling program to use a type that derives from IComparable for a generic parameter K. Visual Studio will provide us intellisense support for this.

1.2 Base Class Derivation

Instead of placing an interface, we can also direct the compiler that the generic parameter type must be derived from a particular base class. For example:

C#
public class MyBaseClass
{
    .....
    .....
}

public class MyGenericClass<T> where T:MyBaseClass
{
    ....
    ....
}

When creating an object of MyGenericclass, we have to use a type that is derived from MyBaseClass. The point to be noted here is that we cannot use System.Delegate or System.Array as constraints for this type. At the same time, we can constrain the base class and interfaces at the same time, but the base class must be written before the interfaces in the constraints list.

1.3 Generic Type Parameter as Constraint

C# allows us to use a generic parameter type as constraint. For example, in the following code block, the generic parameter T must be derived from generic parameter U when actual types are supplied.

C#
public class MyClass<T,K> where T:K
{
    ....
    ....
}

When creating an object of MyClass, we have to be careful in assigning the types for T and K. The type to be assigned to T must be derived from the type that is to be assigned to K. If we ignore this restriction, the compiler will throw a compile time error.

2. Constuctor Constraint

Consider a scenario wherein we have to initialize the generic argument in the generic class itself. In this case, the compiler will not be able to judge whether the actual type supplied for the generic parameter has a default constructor. Consider the example below:

C#
public class MyClass<T>
{
    int item;
    T t;
    public MyClass()
    {
        item=0;
        t=new T();
    }
}

This code will not compile as the compiler does not know whether the actual type supplied for T supports the default public constructor. To overcome this issue, we use the constructor constraint as follows:

C#
public class MyClass<T> where T:new()
{
    int item;
    T t;
    public MyClass()
    {
        item=0;
        t=new T();
    }
}

We can combine the constructor constraint with the derivation constraint, provided the constructor constraint appears last in the constraint list.

3. Value and Reference Type Constraints

We can constrain a generic type to have an argument of any value type like int, bool, enum, or any custom struct as follows:

C#
public class MyClass<T> where T:enum

This will tell the compiler that the type of T is enum. Similarly, we can constrain a generic type argument to use only reference types, as follows:

C#
public class MyClass<T> where T:class

Points to Note

  1. We cannot use a reference/value type constraint with a base class constraint for a generic parameter as the base class constraint itself implies a class.
  2. We cannot use struct and default constructor constraints together on a generic argument as the default constructor itself implies a class.

Inheritance Support to Generics

In C#, we can derive a class from the generic base class, but at the time of defining, we have have to provide a specific type to a generic base class. For example:

C#
public class mybaseClass<T>
{...}
public class myDerivedClass:myBaseClass<bool>
{
    ....
}

However, if we want to create the generic derived class, we can pass the generic argument of the derived class to the base class, as follows:

C#
public class mybaseClass<T>
{...}
public class myDerivedClass<T>:myBaseClass<T>
{
    ....
}

If the base class has defined some constraints, then when defining the derived class, we have to define these constraints again on the derived class in the same sequence. For example:

C#
public class myBaseClass<T> where T:IList
{...}
public class myDerivedClass<T>:myBaseClass<T> where T:IList
{
    ....
}

If the base class uses a generic virtual method, then in the subclass, we must place the actual type when we try to override the virtual method. For example:

C#
public class BaseClass<T>
{
    public virtual T SomeMethod(){...}
}
public class SubClass:BaseClass<int>
{
    public override int SomeMethod(){...}
}

If the subclass is generic, it can also use its own generic type parameters for the override:

C#
public class SubClass<T>: BaseClass<T>
{ 
   public override T SomeMethod()
   {...}
}

We can define generic interfaces, abstract classes, and generic abstract methods, and they will behave like other generic types work.

Generic Methods

In C# 2.0, we can define generic methods as we define classes. They can reside inside a generic class and can reside in a normal class as well. Generic methods give us the flexibility to call a function with different sets of parameter types, and this becomes an important feature when we build utility classes.

C#
public class MyClass<T>
{
    public void SomeMethod<X>(X x)
    {
        ...
    }
}

or in a normal class like:

C#
public class MyClass
{
    public void SomeMethod<X>(X x)
    {
        ...
    }
}

This ability is available for methods only. Properties or indexers can only use generic type parameters defined at the scope of the class.

Generic Static Methods

C# allows us to create generic static methods inside a generic class, but at the time of calling the static methods, we have to replace the generic type parameter with the actual type.

C#
public class MyClass<T>
{
   public static T SomeMethod<x>(T t,X x)
   {..}
}
int number = MyClass<int>.SomeMethod<string>(3,"AAA");

Or rely on type inference when possible:

C#
int number = MyClass<int>.SomeMethod(3,"AAA");

Generic static methods are subject to all constraints imposed on the generic type parameter they use at the class level. As with instance methods, you can provide constraints for generic type parameters defined by the static method:

C#
public class MyClass
{
   public static T SomeMethod<T>(T t) where T : IComparable<T>
   {...}
}

Generic Delegates

C# allows us to declare generic delegates the same way we declare other generic types. If we declare a generic delegate inside a generic class, then the delegate has to use the type specified in the class level, like in the code snippet below:

C#
public class MyClass<T>
{
    public delegate void MyGenericDelegate(T t);
    public void SomeMethod(T t)
    {
        ...
    }
}

At the time of instantiating the class and supplying the actual type for the generic parameter, it affects the delegate as well.

C#
MyClass<int> s=new MyClass<int>();
MyClass<int>.MyGenericDelegate d;
d=MyClass<int>.MyGenericDelegate(s.SomeMethod);
d(2);//Invoking the method with the halp of deligate variable

C# 2.0 provides us the other method to do the same action in a much simplified manner.

C#
MyClass<int> s = new MyClass<int>();
MyClass<int>.MyGenericDelegate d;
d=s.SomeMethod;
d(2);

The compiler is capable of inferring the type of the delegate you assign into, finding if the target object has a method by the name you specify, and verifying that the method's signature matches. Then, the compiler creates a new delegate of the inferred argument type (including the correct type instead of the generic type parameter), and assigns the new delegate into the inferred delegate. Delegates can be defined as generic outside the scope of the class. In this case, when instantiating the delegate, we have to be sure that we supply the actual type for the generic parameter, as seen in the example below:

C#
public delegate void MyGenericDelegate<T>(T t);
public class MyClass
{
    
    public void SomeMethod(int n)
    {
        ...
    }
}
//and this is how it is invoked
MyClass s=new MyClass();
MyGenericDelegate<int> d=new MyGenericDelegate<int>(s.SomeMethod);
d(10);

The constraints can be applied to generic delegates as well in the same way we put these constraints on other places. Generic delegates are very helpful when creating events. We can just create a bunch of generic delegates and use them for event handling throughout the application.

Conclusion

In this article, we have seen how we can use delegates in many different ways, and how easily they can solve our type related issues and make us more productive and help us create reusable and quality code. C# generics are inspired from C++ templates, and provide much advantages as compared to C++ templates. I have still not covered all the features of Generics in the area of Reflection, Event Handling, Attributes, and various other possible areas.

Revison History

  • 23-12-2009: Initial version of article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
GeneralI vote 5, Great! Pin
reborn_zhang24-Dec-09 9:25
reborn_zhang24-Dec-09 9:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.