Generics are introduced in C# 2.0. It is the most powerful features of C#. In C# 1.0, when we design a class that use other types that is not known at defining the structure of the class, then type should be an object type. In the class, we have to typecast the type using boxing and unboxing and then we can use the type.

Benefits of using generics are they are strongly-typed collections as well as provide a higher quality of and a performance boost for code. Generics are very similar to C++ templates but having a slight difference in such a way that the source code of C++ templates is required when a templates is instantiated with a specific type and .NET Generics are not limited to classes only. In fact they can also be implemented with Interfaces, Delegates and Methods. The detailed specification for each collection is found under the System.Collection.Generic namespace.

Generics introduced the concept of type parameters. With the type parameters, we can design a generic class which uses other types with type safety. Generic classes defer the specification of types until the class is instantiated. For e.g. when designing a generic class, we assume this class can work with any type. When we instantiated the class we specify the actual data type which we want to use. For example, we can define int, string, double, or any custom data type. The Generic class can be defined by putting the <T> sign after the class name. It isn't mandatory to put the "T" word in the Generic type definition.

For example, here is the generic Class example:

public class GenericClass<T>
{
    public void DoSomething(T item)
    {
        //work on T item
    }
}

In the above class, we have not defined the actual type of item parameter. On client side, we can declare this Generic class with any type.

GenericClass<int> intClass = new GenericClass<int>();
 
GenericClass<string> stringClass = new GenericClass<string>();
 
GenericClass<CustomClass> customClass = new GenericClass<CustomClass>();

In the above example, we have declared GenericClass with int, string, and CustomClass. <T> in the Generic class is Type Parameter. If we instantiated Generic class with <int>, then we can only pass int in the DoSomething method.

GenericClass<int> GenerClass = new GenericClass<int>();
GenerClass.DoSomething(44);        // pass int parameter
GenerClass.DoSomething("string");  // this statement will not compile as we can pass only ints.

Generics Features

  1. It provides type safety as you can only pass type that is declared at the instantiation time.
  2. Code is reusable and can work with any data type.
  3. We can use Generic with classes, interfaces, events, and delegates.
  4. We can put constraints on generic class, so that client can use only selected types.

Benefits of Generic

  1. Allow write code  library method which are type-safe.
  2. Because of generic being used, compiler can perform compile-time checks on code for type safety.
  3. Faster than using objects as it either avoids boxing/un-boxing or casting from objects to the required reference type.
  4. Allow you to write code which is applicable to many type with the same underlying behavior.

The System.Collection.Generic namespace also defines a number of classes that implement many of these key interfaces.Some are listed below:

  • HashSet<T>
  • LinkedList<T>
  • List<T>
  • Queue<T>
  • Stack<T>
  • ICollection<T>
  • IComparer<T>
  • IEnumerable<T>
  • IEnumerator<T>
  • ILIst<T>

Example of using List<T> class:

public class NodeList<T>
{
    private List<T> nodes;
 
    public NodeList()
    {
        this.nodes = new List<T>();
    }
 
    public void AddNode(T newNode)
    {
        nodes.Add(newNode);
    }
 
    public void DeleteNode(T nodeToRemove)
    {
        nodes.Remove(nodeToRemove);
    }
 
    public void ProcessAllNodes()
    {
        foreach(var node in nodes)
        {
            Console.WriteLine(node.ToString());
        }
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        NodeList<int> nodesOfInt = new NodeList<int>();
        nodesOfInt.AddNode(2);
        nodesOfInt.AddNode(4);
        nodesOfInt.AddNode(6);
        nodesOfInt.AddNode(7);
        nodesOfInt.DeleteNode(7);
 
        nodesOfInt.ProcessAllNodes();
 
        NodeList<string> nodesOfString = new NodeList<string>();
        nodesOfString.AddNode("John");
        nodesOfString.AddNode("Sameer");
        nodesOfString.AddNode("James");
        nodesOfString.AddNode("Rock");
 
        nodesOfString.ProcessAllNodes();
 
        
    }

Output:

 2
 4
 6
 John
 Sameer
 James
 Rock

Generic Constraints

Constraints are validations that we can put on generic Type parameter. At the instantiation time of generic class, if client provides invalid type parameter then compile will give an error.

There are six types of constraints.

  1. where T : struct  - The type argument must be value type except Nullable.
  2. where T : class - The type argument must be reference type(class,delegate,interface or array type).
  3. where T : new() - The type argument must have a public parameter-less constructor.
  4. where T : <base class> - The type argument must be or derive from the specified base class.
  5. where T : <interface> -  The type argument must be or implement the specified interface/s.
  6. where T : U - The type argument supplied for T must be or derive from argument supplied for U.

Constraints examples:

where T: struct example

In the struct constraint, we can only specify a value type in the type argument. Some value types are int, double, decimal, and DateTime.

public class NodeList<T> where T : struct
{
}
 
NodeList<int> lst1 = new NodeList<int>();    //No error, as int is a value type
NodeList<string> lst2 = new NodeList<string>(); //Error as string is a reference type
NodeList<Employee> lst3 = new NodeList<Employee>(); //Error as Employee is a reference type;

where T : class example

In the class constraint, we can only specify reference type in the type argument. Some reference type are string, class, and delegates.

public class NodeList<T> where T : class
{
}
 
NodeList<string> nodesOfString = new NodeList<string>();        // string is a reference type
NodeList<Employee> nodesOfEmployee = new NodeList<Employee>();  // Employee is a reference type
NodeList<EventHandler> nodesOfAction = new NodeList<EventHandler>();    //EventHandler is a delegate and a reference type

where T : new() example

In the new() constraint, we can only specify types which has parameterless constructor like shown below:

public class NodeList<T> where T : new()
{
}
 
    class Program
    {
        static void Main(string[] args)
        {
NodeList<Employee> employeeNodes = new NodeList<Employee>(); //No Error, as Emplyoee has constructor of no parameters.
 
NodeList<Customer> customerNodes = new NodeList<Customer>(); //Error, as Customer has constructor which takes parameters.
             
        }
    }
 
public class Employee
{
    public Employee()
    {
 
    }
}
 
public class Customer
{
    public Customer(string customerName)
    {
 
    }
}

where T : <base class> example

In the <base class> constraint, we can only specify types that in inherit from <base class> like shown below:

public class NodeList<T> where T : BaseEmployee
{
}
public class BaseEmployee
{
 
}
 
public class Employee : BaseEmployee
{
}
 
public class Customer
{
}
 
class Program
{
    static void Main(string[] args)
    {
        NodeList<Employee> employeeNodes = new NodeList<Employee>(); //No Error, as Emplyoee is inherited from BaseEmployee
 
        NodeList<Customer> customerNodes = new NodeList<Customer>(); //Error, as Customer is not inheried from BaseEmployee
 
    }
}

where T : <interface> example

In the <interface> constraints, we can only specify types that implements the <interface> like shown below:

public class NodeList<T> where T : IEmployee
{
}
 
public interface IEmployee
{
}
 
public class Employee : IEmployee
{
}
 
public class Customer
{
}
 
class Program
{
    static void Main(string[] args)
    {
        NodeList<Employee> employeeNodes = new NodeList<Employee>(); //No Error, as Emplyoee implements the IEmployee interface
 
        NodeList<Customer> customerNodes = new NodeList<Customer>(); //Error, as Customer does not implements the IEmployee interface
 
    }
}

where T : U example

In this constraint, there are two type arguments T and U. U can be a interface, abstract class, or simple class. T must inherit or implements the U class like shown below:

public class NodeList<T, U> where T : U
{
    public void DoWork(T subClass, U baseClass)
    {
 
    }
}
 
public interface IEmployee
{
}
 
public class Employee : IEmployee
{
}
 
class Program
{
    static void Main(string[] args)
    {
        NodeList<Employee, IEmployee> employeeNodes = new NodeList<Employee, IEmployee>();
    }
}

Reference