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:
- Add a data member to store the number.
- Provide a constructor to initialize the number.
- 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.
- 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).
- Code re-use. Because we can use the same single template for multiple data types.
- 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.