C# Generics

C#  C#  generics 

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.

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. For defining the Generic class, we use <T> after the class name.

Below is the example of Generic class.

public class GenericClass<T>
{
    public void DoWork(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 DoWork method.

GenericClass<int> intClass = new GenericClass<int>();
intClass.DoWork(44);        // only pass int parameter
intClass.DoWork("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. I'll this feature later.

.NET provides several generic classes and interfaces. These are exists in System.Collections.Generic namespace. 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("Lory");
        nodesOfString.AddNode("Julia");
        nodesOfString.AddNode("Lerman");
        nodesOfString.AddNode("James");

        nodesOfString.ProcessAllNodes();

        /// Result
        /// 2
        /// 4
        /// 6
        /// Lory
        /// Julia
        /// Lerman
        /// James
    }
}

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  - Type argument must be a value type
  2. where T : class - Type argument must be a reference type
  3. where T : new() - Type argument must have a public parameterless constructor.
  4. where T : <base class> - Type argument must inherit from <base class> class.
  5. where T : <interface> -  Type argument must implement from <interface> interface.
  6. where T : U - There are two type arguments T and U. T must be inherit from 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> lst = new NodeList<int>();    //No error, as int is a value type
NodeList<string> lst1 = new NodeList<string>(); //Error as string is a reference type
NodeList<Employee> lst2 = 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>();
    }
}

Final Words

C# Generics classes are powerful way of creating the types that use other types. Generic classes make classes reusable and with type-safety.

Join our newsletter and get an occasional email with a technology and dotnetpattern.com news update.