Let's look at an example to see how generics work. Suppose that you need to implement your own custom stack class. A stack is a last-in, first-out (LIFO) data structure that enables you to push items into and pop items out of the stack. One possible implementation is:
public class MyStack {
private int[] _elements;
private int _pointer;
public MyStack(int size) {
_elements = new int[size];
_pointer = 0;
}
public void Push(int item) {
if (_pointer > _elements.Length - 1) {
throw new Exception('Stack is full.');
}
_elements[_pointer] = item;
_pointer++;
}
public int Pop() {
_pointer--;
if (_pointer < 0) {
throw new Exception('Stack is empty.');
}
return _elements[_pointer];
}
}
In this case, the MyStack
class allows data of int type to be pushed into and popped out of the stack. The following statements show how to use the MyStack
class:
MyStack stack = new MyStack(3);
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine(stack.Pop()); //---3---
Console.WriteLine(stack.Pop()); //---2---
Console.WriteLine(stack.Pop()); //---1---
As you can see, this stack implementation accepts stack items of the int
data type. To use this implementation for another data type, say String
, you need to create another class that uses the string
type. Obviously, this is not a very efficient way of writing your class definitions because you now have several versions of essentially the same class to maintain.
A common way of solving this problem is to use the Object
data type so that the compiler will use late-binding during runtime:
public class MyStack {
private int _pointer;
public MyStack(int size) {
_pointer = 0;
}
if (_pointer > _elements.Length - 1) {
throw new Exception('Stack is full.');
}
_elements[_pointer] = item;
_pointer++;
}
public object Pop() {
_pointer-- ;
if (_pointer < 0) {
throw new Exception('Stack is empty.';
}
return _elements[_pointer];
}
}
One problem with this approach is that when you use the stack class, you may inadvertently pop out the wrong data type, as shown in the following highlighted code:
MyStack stack = new MyStack(3);
stack.Push(1);
stack.Push(2);
//---invalid cast---
Because the Pop()
method returns a variable of Object
type, IntelliSense cannot detect during design time if this code is correct. It is only during runtime that when you try to pop out a string
type and try to typecast it into an int type that an error occurs. Besides, type casting (boxing and unboxing) during runtime incurs a performance penalty.
To resolve this inflexibility, you can make use of generics.
Generic Classes
Using generics, you do not need to fix the data type of the items used by your stack class. Instead, you use a generic type parameter (<T>
) that identifies the data type parameter on a class, structure, interface, delegate, or procedure. Here's a rewrite of the MyStack
class that shows the use of generics:
private int _pointer;
public MyStack(int size) {
_pointer = 0;
}
if (_pointer > _elements.Length - 1) {
throw new Exception('Stack is full.');
}
_elements[_pointer] = item;
_pointer++;