Generics let you parameterize code over types in the same way that arguments let you parameterize it over values. That probably doesn't explain a whole lot, so lets go through it a step at a time:
Imagine you want a program to print "I like bunnies". You write this:
static void Main()
{
ILikeBunnies();
}
void ILikeBunnies() { Console.WriteLine("I like bunnies"); }
All well and good. But you also like cheese, so now you have:
static void Main()
{
ILikeBunnies();
ILikeCheese();
}
void ILikeBunnies() { Console.WriteLine("I like bunnies"); }
void ILikeCheese() { Console.WriteLine("I like cheese"); }
You notice that your two functions are almost identical. You want to reuse the same function, but provide different values for what you like:
static void Main()
{
ILike("bunnies");
ILike("cheese");
}
void ILike(string what) { Console.WriteLine("I like " + what); }
This is what function arguments are for: they let you reuse the same code with different values.
Generics are like that, but instead of passing in different values, you pass in types. Lets say you've got code that needs to bundle two strings into an array:
static void Main()
{
string[] s = Pair("a", "b");
}
string[] Pair(string a, string b) { return new string[] { a, b }; }
Fine and dandy. Later you realize you also need to bundle ints into an array:
static void Main()
{
string[] s = Pair("a", "b");
int[] i = Pair(1, 2);
}
string[] Pair(string a, string b) { return new string[] { a, b }; }
int[] Pair(int a, int b) { return new int[] { a, b }; }
Just like before, we can see there's a bunch of redundancy there. What we need is a function that returns a pair of whatever and a way to pass in the type of what we want a pair of. This is what generics are for:
static void Main()
{
string[] s = Pair<string>("a", "b");
int[] i = Pair<int>(1, 2);
}
T[] Pair<T>(T a, T b) { return new T[] { a, b }; }
The angle brackets let you pass in a type to a function in the same way parentheses let you pass in values. "T" here is the name of a type argument just like "what" was the value argument above. Any place T appears in the function will be replaced with the actual type you pass in (string and int in the example).
There's a bunch of stuff you can do with generics beyond this, but that's the basic idea: generics let you pass types into functions (and classes) in the same way arguments let you pass in values.