Please note that this is an “Intermediate” article; it therefore assumes you have read all previous “Beginner” articles, and you know how to create an instance of a class.
Introduction
Generics allow a type or method to operate on objects of varying types, while providing compile-time type safety. They can be used to achieve parametric polymorphism – a way to make a language more expressive, while continuing to maintain full static type safety. Through the use of generics, you can – as the name implies – generically define a member (or method) in a class, which would then be valid for all related types. Alternatively, you can pass data (of any type) to a function whose parameter has a template. Templates are commonly considered a compile-time feature. Unlike polymorphism through interfaces, and inheritance, there is virtually no overhead when using templates. I will discuss this later in detail, but strictly speaking, this is because templates are normally transformed into strongly-typed source code during compilation.
In this detailed article, I will tell you how to implement Generics in C#, the benefits of using this programming model, and how it is different from templates and generics in C++ and Java. Let’s start the journey!
How to Implement
In my previous article on operator overloading, I presented an example class called Vector3f. Continuing where we left off, suppose we want a class which holds three-point vector values (x, y and z). For this example, we want it to be generic and accept any data type, including integer, float and double. In order to do that, we would typically need to create different classes for different data types. Fortunately, by using generics, we would only need to define a single class. This class would include generic semantics, which implement the same methods for all the data types. We would, of course, only implement these methods when the operation on them makes sense. Let’s call our generic vector class Vector3, as we did before. Here is how we would define it generically:
1 2 3 4 5 6 7 8 9 10 11 |
class Vector3 <T> { public T x, y, z; public Vector3(T x, T y, T z) { this.x = x; this.y = y; this.z = z; } } |
The syntax to declare a class as generic is:
1 |
<class name<data type> or specifier like 'var' for type inference> <object name> = new <class name>()<data type> |
With that in mind, we can use our generic class in the program as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; class Generics { public static void Main() { Vector3<int> veci = new Vector3<int> (); // This creates an object of Vector3 with data type "int" veci.x = 20; Console.WriteLine(veci.x); // This should print 20 var vecf = new Vector3<float> (); // This creates an object of Vector3 with data type "float" vecf.x = 15.5; Console.WriteLine(vecf.x); // This should print 15.5 var vecd= new Vector3<double> (); // This creates an object of Vector3 with data type "double" vecf.x = 18.50; } } |
As you can see, we can easily create a Vector3 object of any data type we want! This greatly reduces the need to create separate classes for different types. Note, however, that you cannot use operators against generic data types. This is because, at the time of compilation, the compiler has no way of knowing if such operations would make sense. This is a limitation of generics in C# (which I will explain later). However, you can still manually overload operators in a class if you are sure that people will only use types that make sense. See this for more information.
Generic Methods
Consider a simple method which prints the passed argument to the console. Without generics, you would need to define a separate method for each data type (int, float, string, etc). All of these methods would have the same name, which is called function overloading.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static void PrintInput(int i) { Console.WriteLine(i); } static void PrintInput(float i) { Console.WriteLine(i); } static void PrintInput(string i) { Console.WriteLine(i); } |
You can see that the actual implementation is the same for all the methods, so typing the above code appears to be a waste of time. However, C# is a strongly-typed language, and you obviously can’t pass a double in where an int argument is supposed to be… so all of that code seems necessary. Fortunately, through generics, you need to only define a single method:
1 2 3 4 |
public static void PrintInput<T>(T i) { Console.WriteLine (i); } |
Generic Interfaces
An interface can be generic too. Consider the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
interface IVector3<T> { T GetX(); } class Vector3f: IVector3<float> { public float x, y, z; public float GetX() { return x; } } class Vector3i: IVector3<int> { public int x, y, z; public int GetX() { return x; } } |
How do Generics Work?
As I previously mentioned, generics is a compile-time feature, which means it works during the compilation of a program. It transforms generic classes, interfaces, and functions into their equivalent strongly-typed counterparts.
1 2 3 4 |
class Vector3<T> { public T x, y, z; } |
During an object’s initialization, the compiler will transform the above code into the below code:
1 2 3 4 5 6 7 8 9 |
class Vector3 { public int x, y, z; } class Vector3 { public float x, y, z; } |
However, please note that this may result in a slight overhead during execution, since the types are being substituted on the fly.
Difference from Java Generics
Java also has the concept of generics. Although it may appear similar at first glance, its implementation is actually quite different. Generics (in Java) provides compile-time safety for type-correctness, but it is considered a run-time feature and is therefore similar to inheritance-polymorphism in practice.
In Java, there is a process called type erasure, through which all type information is removed during compilation. This means that, during run-time, there is no way to tell what type a generic object was when it was originally instantiated. This is a restriction that many programmers are dissatisfied with, since they cannot implement algorithms which need to know the original type of an object. For example, they cannot do things like:
1 2 3 4 5 6 7 8 9 |
class MyClass<T> { public static void MyMethod(Object item) { if (item instanceof T) { // Compiler error! } } } |
During run-time, there is no way to tell what the original type of T was. Additionally, Java’s “type erasure” actually produces more overhead, because behind-the-scenes the compiler is casting the type itself, similar to the following:
1 2 |
ArrayList<Vector3> foo = new ArrayList<Vector3>(); Vector3 vec = (Vector3)foo.get(1); |
See this article for more information.
Difference from C++ Templates
In C++, templates are purely a compile-time feature. Although it results in less code being initially written, the resulting program is actually bigger in filesize due to the boilerplate code generated during compilation. Templates in C++ are more powerful than in C# and Java, but they are very complex nonetheless. Both provide support for parameterized types; C++ provides substitutions of types during compile-time, whereas in C#, substitutions are performed at runtime.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<typename T> class Vector3 { public: T x, y, z; }; int main() { Vector3<int> vec; // define Vector3 of type 'int' during compile time vec.x = 20; } |
In C#, you cannot call arithmetic operators on generic types, although you can call user defined operators. In C++, the following code is valid:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
template<typename T> class Vector3 { public: T x, y, z; T getLength() { return sqrt(x * x + y * y + z * z); // The operator '*' is valid here } Vector3<T>(T x, T y, T z) { this->x = x; this->y = y; this->z = z; } }; int main() { Vector3<float> vec(15.5, 12.0, 13.2); vec.getLength(); } |
C# supports neither explicit specialization nor partial specialization, which makes generics less-than-useful in many cases. See this for more information.
Conclusion
All in all, generics is a useful feature, especially in libraries. Its usefulness cannot be dismissed when creating a library for other programmers, considering it’s implemented in C# collections. I find it easier to achieve polymorphism through generics than through interfaces. What do you think?