C# defines generic data structures, often called simply generics, to create non-specific - generic - data types, which can be used almost as a wildcard data type, that will be later known. In other words, they refer to generalistic data types, instead of concrete ones. To better understand this concept, consider the following example:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
using
System;
namespace
HelloWorld
{
class
Program
{
static void Main(string[] args)
{
PrintValue(2);
Console.ReadLine();
}
static void PrintValue(int param)
{
Console.WriteLine("The value is " + param);
}
}
}
|
All fine so far, the PrintValue method lets us print the value of any integer type, but what would happen if we suddenly needed to use it with real type values, say, decimals? You do know that a decimal has a much wider storage range, not to mention a fractional part, so we can't just send it to an integer parameter. True, we could create an overload of the method:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using
System;
namespace
HelloWorld
{
class
Program
{
static void Main(string[] args)
{
PrintValue(2);
PrintValue(2.3M);
Console.ReadLine();
}
static void PrintValue(int param)
{
Console.WriteLine("The value is " + param);
}
static void PrintValue(decimal param)
{
Console.WriteLine("The value is " + param);
}
}
}
|
But the problem remains: if we need to display values of floats, we need a new overload, and so on, and so on. Making the function accept parameters of type object, which can accept any kind of value, is definitely not an option, because once converted to an object, we no longer know of what type the value is (and it is also rather inefficient, due to a process called boxing-unboxing, of which we will learn in the future). Aside of that, we have a lot of code duplication, and if you pay a closer attention to the two examples, you will notice that except for the data types - int, as oposed to decimal - the two methods are identical. That's a very inefficient way of writing code!
The above problem can be solved with a programming concept called generic types, or simply generics. Generics allow us to use an unknown data type, sort of like a wildcard, a question mark if you will, which only gets known when we actually want to use it. If we rewrite our code in such way as to make use of generics, it would look like this:|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
using
System;
namespace
HelloWorld
{
class
Program
{
static void Main(string[] args)
{
PrintValue<int>(2);
PrintValue<decimal>(2.3M);
Console.ReadLine();
}
static void PrintValue<WildcardType>(WildcardType param)
{
Console.WriteLine("The value is " + param);
}
}
}
|
The first thing to notice here is the WildcardType thingy, where the type of the parameter of the PrintValue method should be. It doesn't say int, decimal, or any other known C# data type. As far as I know, there is no C# data type named WildcardType. So, why is the compiler not complaining? And even more so, why is the program working, if we run it?
The answer lies in the second thing that you should notice, the fact that the method PrintValue actually became PrintValue<WildcardType>. That thing between the angle brackets is the generic type, it's where we would place the wildcard, the "question mark", as it were. The method PrintValue doesn't know what type is WildcardType. Could be int, could be decimal, could be anything else. Why? Because it's a generic data type, and it accepts anything. The moment when it actually does find out what type that "question mark" is is when we call the method PrintValue, and we must give a value for the generic parameter: PrintValue<int>(2). The int between the angle brackets is telling the compiler that WildcardType type is actually of type int. Then, and only then, when we called the method, the compiler knew the type of the method parameter. When we called it with a decimal, like this: PrintValue<decimal>(2.3M), then WildcardType became of type decimal. And so on, and so forth. We can now call the method PrintValue<WildcardType> with any type, and WildcardType becomes that type.
Once the concrete type of a generic type is specified in an instance, it can no longer be changed later on (for example, once you specify that a generic array is of type int, you can not change it to one of type string after that; instead, you need to declare a new instance of that generic array, where you need to specify the concrete type again).
Most of the times, you will see that programmers prefer to use very specific names for the names of the generic members, the most common one being T, which is a shortcut for "Type", any type. Something like this:
|
1
2
3
4
|
static void PrintValue<T>(T param)
{
Console.WriteLine("The value is " + param);
}
|
If more than one type of parameter needs to be passed, the naming convention continues with the alphabet letters that follow after T, and the generic types definitions are separated with comma:
|
1
2
3
4
|
static void PrintValue<T, U>(T firstParam, U secondParam)
{
Console.WriteLine("The value is " + firstParam);
}
|
Generic types and parameters can be used just like any other C# type. For instance, we can create an array of type U:
|
1
2
3
4
5
|
static void PrintValue<T, U>(T firstParam, U secondParam)
{
U[] someArray;
Console.WriteLine("The value is " + firstParam);
}
|
Generic types can even be used as return types of functions:
|
1
2
3
4
5
|
static U PrintValue<T, U>(T param)
{
U whatever;
return whatever;
}
|
Finally, generic types can also be used on classes, structures, records, interfaces and delegates. Here is an example of declaring a generic class:
|
1
2
3
4
5
6
7
8
9
10
11
|
class GenericClass<T>
{
T genericField;
T GenericProperty { get; set; }
void GenericMethod(T param)
{
}
}
|
In this case, the generic type(s) must be specified when we instantiate the class, and they are "visible" in the scope of the entire class (they can be used for anything in that class):
|
1
|
GenericClass<int> someGenericClassInstance = new GenericClass<int>();
|
The same rules apply in this case too: multiple generic types can be specified and they are separated by comma, etc.
If the method needs a second generic type parameter, different from the class generic types, it only needs to declare that one:
|
1
2
3
4
5
6
7
|
class GenericClass<T>
{
void GenericMethod<U>(T firstParam, U secondParam)
{
}
}
|
It is a good idea to give more descriptive names to your generic types, instead of just T, U, etc. The name should still start with T, to indicate that it is a type, but should continue with a name that is descriptive of the purpose of that generic type. For instance:
|
1
2
3
4
5
|
static TReturn PrintValue<TParam, TReturn>(TParam param)
{
TReturn whatever;
return whatever;
}
|
It is now clear from first glance that the first generic type is intended as the parameter type of the function, while the second refers to the return type.