(C# Language) Generics

In this tutorial we take a hypothetical problem to build towards the concept of generics. First we explore a few bad solutions. Then we explain a perfect solution by generics. That leads us to list the benefits of using generics. We conclude with the constraints on generic parameters.
(Rev. 31-Oct-2024)

Categories | About |     |  

Parveen,

Table of Contents (top down ↓)

The Problem

Suppose we need a class that can hold a number and provides it back when required. Please note this problem is purely hypothetical. I am using it for simplifying my explanation of a concept.

This could, roughly, be our plan:

  1. Add a data member to store the number.
  2. Provide a constructor to initialize the number.
  3. Add a function to return the number.

The plan is simple. Right? But what about the question of data type? The number could be a float, int, double - and for that matter - any numeric type. Various solutions could strike us.

Video Explanation (see it happen!)

Please watch the following youtube video:

First bad Solution - Create Multiple Classes

One solution would be to create different classes - one for an int, one for a float, and so on.

But that would be too stupid!

Writing too many classes, with each one differing only in the data-type, would be an absurdly laborious task.

This approach leads to maintenance problems also. Even a slight change of algorithm would involve changes in too many classes, and possibly at too many places.

Second bad Solution - Use Object Type

Another solution would be to use an Object datatype instead of a specific datatype:


// a class to store a number of any 
// type. it uses an object type everywhere 
// so that it can handle an int, float, double, etc., 
// any datatype 

class C2
{
  Object mNumber;

  public C2(Object num) => mNumber = num;

  public Object GetNumber () => mNumber;
    
}

This class can store any numeric type because all data-types in DOTNET inherit from the object base class.

The constructor accepts an object type and the GetNumber function returns an object type.

We can use this class to store an int type as shown.


// using the above class to  
// hold and retrieve an int type 

static void Main()
{
  // boxing conversion occurs 
  C2 m1 = new C2(2);

  // un-boxing conversion occurs 
  int i = (int)m1.GetNumber();
}

// there is a fundamental  
// performance issue in this solution 

The constructor boxes an int value into an Object type.

The function GetNumber unboxes the object to an int value.

The class appears perfect on the face of it. But there is a fundamental performance issue here.

Each operation requires a boxing and unboxing conversion, which is a very costly operation in terms of performance. Measurements indicate a 200% difference in performance. Experts always recommend us to avoid boxing and unboxing conversions. They put a heavy load on the garbage collector.

Solution with Generics

The above problem is best solved by using Generics.

Have a look at this solution:


// blueprint template 
class C2<T>
{
  T mNumber;

  public C2(T num) => mNumber = num;

  public T GetNumber () => mNumber;

}

// generic class doesn't make a  
// commitment on a specific data-type 

// It uses a placeholder T for data-type 

// a client should specialize this class 
// for specific data-type: C2<float> 

This class is a blueprint template. It is NOT a class. It doesn't make a commitment on a specific data-type. It uses a placeholder T for later use.

If you want to store a float type, then you can instruct the compiler and runtime to create an object corresponding to this template. It happens at runtime and is mostly a behind-the-scenes act. This process is called specialization.


// specialize the generic for a float 

// the compiler and runtime create an 
// object out of the generic template 
// it's done for you behind-the-scenes 

static void Main()
{
  C2<float> m1 = new C2<float>(2.0f);

  float f = m1.GetNumber();
}

// Note: C++ templates are mostly like macros 
// but C# are like classes  
// (Anders Hejlsberg with Bill Venners and Bruce Eckel) 
// https://www.artima.com/articles/generics-in-c-java-and-c 

Notice the use of angled brackets for specifying the data-type.

Benefits of Generics

We can now list the benefits of generics.

  1. Generics help us avoid boxing/unboxing for value types and casting for reference types. It gives a typical performance benefit of 100% to 200%. Refer this: https://learn.microsoft.com/en-us/previous-versions/ms379564(v=vs.80).
  2. Code re-use. Because we can use the same single template for multiple data types.
  3. Ease of maintenance, again because of a single code base. Future changes are easy.

Constraints in Generics

A C# compiler allows you to apply constraints to the generic types.

Constraints can be placed on the generic types. The following restricts the generic to a value type. We could have restricted T to a reference type by using where T : class.


// restricts T to a value type 

class C2<T> where T : struct
{
  T mNumber;

  public C2(T num) => mNumber = num;

  public T GetNumber () => mNumber;
    
}

// restrict to ref type 
// where T : class 

The following restricts T to a class with a public default constructor where T : new()


// restricts T to a class 
// with a public default ctor 

class C2<T> where T : new()
{
  T mNumber;

  public C2(T num) => mNumber = num;

  public T GetNumber () => mNumber;
    
}

See this link for more on restrictions - MSDN Generic Constraints


This Blog Post/Article "(C# Language) Generics" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.