When we write a method in C#, we often want to specify an input and a return for it. Since C# is a strongly typed language, it likes to know exactly what data type these inputs and returns are.
Let’s take an example, which takes a 2D array of data and returns a specified row as a list:
public List<string> GetRowOf2dArray(string[,] array, int rowNo)
{
List<string> rtnlist = new List<string>();
for (int i = 0; i < array.GetLength(0); i++)
{
rtnlist.Add(array[i, rowNo]);
}
return rtnlist;
}
This is all well and good if we only want to use strings. But what happens if, later on, we want to use the function with a list of doubles?
We might decide to create a copy of the method:
public List<double> GetRowOf2dArray(double[,] array, int rowNo)
{
List<double> rtnlist = new List<double>();
for (int i = 0; i < array.GetLength(0); i++)
{
rtnlist.Add(array[i, rowNo]);
}
return rtnlist;
}
Now, at the interface level, we can extract a row whether we have a matrix of doubles or of strings, and return them in the correct data type.
string[,] branchPaths = new string[5, 5];
for (int i = 0; i < branchPaths.GetLength(0); i++)
{
for (int j = 0; j < branchPaths.GetLength(1); j++)
{
branchPaths[i, j] = "I am cell " + i.ToString() + ", " + j.ToString() + ".";
}
}
var rtnList = new List<string>();
rtnList = tst.GetRowOf2dArray(branchPaths, 0);
But, we’re already running into problems. We basically had to copy and paste the GetRow…() method above, which is already bad practice. What if we want to change something, or we find there’s a bug? We’d have to make the change in every copy of GetRow…(). And what if we want to use the method with yet another data type, such as an array of integers? We’d have to keep copying and pasting for every data type.
One way around this is instead to write the method using objects as the type. Since all types inherit from the object type, we can write the method once and cast the specific data types. A very simple example is:
public bool IsEqual(object v1, object v2)
{
if (v1 == v2) return true;
else return false;
}
double a = 2;
double b = 3;
bool areTheyEqual = IsEqual(a, b); //returns false
However, this gives a lot of problems, which are explained in more depth here. Basically, using objects means that we lose performance due to casting, and we lose control since all data types can be casted to and from objects. What we want is a way to create a method suitable for multiple data types, but where we still can feel safe that the user won’t break our method with a strange data type.
Generics
This is solved with generics. As well as feeding in parameters to our methods, we can also tell the method what kind of data type we want to use it with.
This will already be familiar if you have worked with lists:
List<double> myList = new List<double>();
myList.Add(1.5);
myList.Add(5.232);
The bit in the angle brackets is the type – we can’t create a list without telling it what kind of data the list will contain. By writing <double>, we are saying that every item in the list will be a double. And if we wanted to create a list of strings, we don’t need to call a different method or different class, but we can still do it by calling the same class by specifying the type as string.
In short, using generics allows you to write your method once for a wide range of data types.
Back to the matrix example – in our class:
public List<T> GetRowOf2dArray<T>(T[,] array, int rowNo)
{
List<T> rtnlist = new List<T>();
for (int i = 0; i < array.GetLength(0); i++)
{
rtnlist.Add(array[i, rowNo]);
}
return rtnlist;
}
int[,] branchPaths = new int[5, 5];
for (int i = 0; i < branchPaths.GetLength(0); i++)
{
for (int j = 0; j < branchPaths.GetLength(1); j++)
{
branchPaths[i, j] = i*100 + j;
}
}
var rtnList = new List<int>();
rtnList = tst.GetRowOf2dArray<int>(branchPaths, 2);
T is the placeholder for the type. When the user calls the method, T will hold a data type, such as string or int. In this example, both an input (T[,]) and the return type (List<T>) have data types that must be carried by the user. The place where the user specifies the data type carried by T is in GetRowOf2dArray<T>. So when the user calls the method, they only need to specify T once. T can then be used wherever necessary throughout the method.
References
MSDN – An introduction to C# generics
MSDN – Generics (C# programming guide)