Generics in C#

In this article, we will see what generics are, what problem they solve, what are generic classes, what are generic methods and what are generic collections.

The Problem

Let’s assume we want to create a class named SampleList which should store a list of integer values. In the real world, this class should offer methods like AddItem(int item), RemoveItem(int item), GetItem(int index) and so on but for the sake of simplicity let’s assume that we will be implementing only AddItem(int item) and GetItem(int index) functions.  Check out the code below to see how to implement it.

public class SampleList<T>
{
    public System.Collections.ArrayList _list;

    public SampleList()
    {
        _list = new System.Collections.ArrayList();
    }
    public void AddItem(int item)
    {
        _list.Add(item);
    }
    public int GetItem(int index)
    {
        return Convert.ToInt32(_list[index]);
    }
}

This class works fine and serves its purpose, however, there are two big problems with this code.

  1. Assume that the business requirements change at a later stage of the project and now you need to store string objects in the list. This code will fail to fulfill the requirement as it allows only integer values to be stored in the list. We must write another similar class for storing the string values instead of integers. What if we need to store long, double and float also. The problem here is we need to write a separate class for each datatype. This is a lot of code duplication and if you encounter a bug, you will have to fix it in multiple locations.
  2. Internally, our data element “_list” is storing all the items as Objects which is the parent class of all value and reference types in C#. Whenever we are adding an Item to the list using the “AddItem” method, the integer value is getting boxed and whenever we are accessing an integer item using the “GetItem” method, the integer value is getting unboxed. Boxing and unboxing have a performance penalty.

The Solution

The best solution to the problem discussed above is to use Generics. Generics allow us to define type-safe classes without compromising performance, or productivity. We implement the classes, methods, and/or other code elements only once as generic code. Using generics, we can declare and use them with any type. It allows us to delay the specification of the data type of programming elements in a class or a method until it is used in the program. In other words, generics allow us to write a class or method that can work with any data type.

Generics are type-safe which offers us three huge advantages;

  1. We write the code only once which can be used for any data type meaning we satisfy our first concern that was code re-usable and DRY.
  2. There is no Boxing and Unboxing needed, therefore they offer better performance.
  3. Because they are type-safe and there is no implicit or explicit type conversion required, usage of generic classes reduces the probability of software crashes significantly compared to that of non-generic code items.

Defining Generic Classes

To define a generic class, we write the specifications with substitute parameters for data types. To do so, we use the < and > brackets, enclosing a generic type parameter. For example, to make the SampleList class a generic class, we can define it as below.

public class SampleList<T>
{
    public List<T> _list;
    public SampleList()
    {
        _list = new List<T>();
    }
}

Notice that <T> is added at the end of SimpleList in the declaration. This allows us to pass the type of the items we need to store in the list at the time of declaring the objects. The object of SimpleList will allow to add or retrieve the objects of only that type. The code below shows you how to create our SampleList object to take a string or integer.

static void Main(string[] args)
{
    var sampleStingList = new SampleList<string>();
    var sampleIntegerList = new SampleList<int>();
}

Generic Methods

Now that we have changed the SampleList class from a simple one to a generic class its time to change those methods to generic types. Take a look at the code below.

public class SampleList<T>
{
    public List<T> _list;

    public SampleList()
    {
        _list = new List<T>();
    }

    public void AddItem(T item)
    {
        _list.Add(item);
    }

    public T GetItem(int index)
    {
        return _list[index];
    }
}

In the above code, the AddItem method takes an argument of T instead of int. This means the argument should have the same type which is passed to the constructor while creating the object of the SampleList class. With these changes, the main method can be changed as follows.

static void Main(string[] args)
{
    var sampleStringList = new SampleList<string>();
    var sampleIntegerList = new SampleList<int>();

    sampleStringList.AddItem("Hello world");
    sampleIntegerList.AddItem(0);

    var stringVariable = sampleStringList.GetItem(0);
    var integerVariable = sampleIntegerList.GetItem(0);
}

Another Example

Let’s consider another example. Lets say you want to create a static method that takes two arguments as reference types and swaps their values. We could create a generic class for this purpose with a static method but it doesn’t make any sense to create a generic class. This is because we don’t want to store any items on the class level. A better way to do this is to write a generic method instead of a generic class. See below.

public class Swap
{
    public static void SwapValues<T> (ref T firstItem, ref T secondItem)
    {
        T temp = firstItem;
        firstItem = secondItem;
        secondItem = temp;
    }
}

In the above example, you will notice that it’s a non-generic class but the SwapValues method is a generic method. This will work just fine.

Constraints on Type Parameters

Now that we know what generics are and how to use them, let’s get into some more complex problems. Let’s assume we need to create a class that offers a method to compare two integers and return the one having the bigger value. The implementation of the class will be as follows;

public class Comparison
{
    public int MaxItem(int a, int b)
    {
        return a > b ? a : b;
    }
}

Pretty simple. But what if the same operation is to be done for type long? Not a problem. We know it can be done by using a generic method. So let’s write a generic version of the MaxItem method.

public class Comparison
{
    public T MaxItem<T>(T a, T b)
    {
        return a > b ? a : b;
    }
}

This code will not compile. We have greater than (>) comparison operator in this method which works for integer variables but it doesn’t work for all the types C# offers. To make this code work, we need to apply some limitations here. We should restrict the method to accept the arguments of only those types that implement the IComparable interface.

Note: All objects that can be compared implement the IComparable interface, you can read more about there here.

The limitations that can be applied to generic classes, interfaces or methods are called constraints.

In our example, we needed the arguments of the MaxItem method to be of a type that implements the IComparable interface. Let’s see how it can be done.

public class Comparison
{
    public T MaxItem<T>(T a, T b) where T : IComparable
    {
        return a.CompareTo(b) > 0 ? a : b;
    }
}

In the above code, we are using “where T : IComparable” to tell the compiler that this generic method will only accept parameters that implement the IComparable interface. This generic method can now accept values of type long and int.  Now let’s update our main method to see how we can use it with both integer and long;

static void Main(string[] args)
{
    var comparison = new Comparison();
    int intValue = comparison.MaxItem<int>(1, 2);
    long longValue = comparison.MaxItem<long>(1, 2);
}

Constraints Available

Below are 4 more constraints that are available to use in situations like above.

where T: struct

This constraint tells the generic class or method that the specified type should be a value type like integer, float, decimal and so on.

where T: class

This constraint tells the generic class or method that the specified type should be a class or reference type like SampleList or List<T>.

where T: new()

This constraint tells the generic class or method that the specified type should be a class or reference type and it should have a default constructor like StringBuilder.

where T: <base class name>

This constraint is exactly like the one we implemented for the MaxItem method of our Comparison class with the only difference that instead of interface, the specified type must be or derived from the specified class.

In this article, we covered what generics are and what problem do they solve. We discussed where they should be used, how to create generic classes and methods, what are generic collections and what are constraints in generics. If you found this article helpful, be sure to check out my C# Roadmap.