First of all, let's clarify two things. This is the definition of a general method:
T M<T>(T x) { return x; }
This is a generic type definition:
class C<T> { }
Most likely, if I ask you what M , you will say that this is a general method that takes T and returns a T This is absolutely correct, but I suggest another way to think about it - there are two sets of parameters. One of them is a type T , the other is an object x . If we combine them, we know that in aggregate this method takes only two parameters.
The currying concept tells us that a function that takes two parameters can be converted to a function that takes one parameter and returns another function that takes another parameter (and vice versa). For example, here is a function that takes two integers and returns their sum:
Func<int, int, int> uncurry = (x, y) => x + y; int sum = uncurry(1, 3);
And here is the equivalent form, where we have a function that takes one integer and produces a function that takes another integer and returns the sum of these above integers:
Func<int, Func<int, int>> curry = x => y => x + y; int sum = curry(1)(3);
We switched from one function that takes two integers to have a function that takes an integer and creates functions. Obviously, these two are not literally the same in C #, but they are two different ways of saying the same thing, because passing the same information will ultimately lead you to the same final result.
Currying makes it easier for us to talk about functions (it's easier to talk about one parameter than two), and this allows us to know that our conclusions are still relevant for any number of parameters.
Consider for a moment that on an abstract level this is what happens here. Let's say M is a “superfunction” that takes type T and returns a regular method. This return method takes a value of T and returns a value of T
For example, if we call the superfunction M with an int argument, we get a regular method from int to int :
Func<int, int> e = M<int>;
And if we call this ordinary method argument 5 , we get a 5 back, as expected:
int v = e(5);
So, consider the following expression:
int v = M<int>(5);
Now you see why this can be considered as two separate calls? You can recognize a call to a superfunction because its arguments are passed to <> . This is followed by a call to the return method, where the arguments are passed to () . This is similar to the previous example:
curry(1)(3);
And similarly, defining a generic type is also a superfunction that takes a type and returns a different type. For example, List<int> is a call to the List super function with an int argument that returns a type containing a list of integers.
Now that the C # compiler encounters a regular method, it compiles it like a regular method. He does not try to create different definitions for different possible arguments. So this is:
int Square(int x) => x * x;
compiles as is. It does not compile as:
int Square__0() => 0; int Square__1() => 1; int Square__2() => 4;
In other words, the C # compiler does not evaluate all possible arguments for this method to embed them in the final exacutable - rather, it leaves the method in its parameterized form and hopes that the result will be evaluated at runtime.
Similarly, when a C # compiler encounters a superfunction (a general method or type definition), it compiles it as a superfunction. He does not try to create different definitions for different possible arguments. So this is:
T M<T>(T x) => x;
compiles as is. It does not compile as:
int M(int x) => x; int[] M(int[] x) => x; int[][] M(int[][] x) => x; // and so on float M(float x) => x; float[] M(float[] x) => x; float[][] M(float[][] x) => x; // and so on
Again, the C # compiler trusts that when this superfunction is called, it will be evaluated at runtime, and a regular method or type will be created from this evaluation.
This is one of the reasons why C # benefits from having a JIT compiler as part of its runtime. When a superfunction is evaluated, it produces a completely new method or type that was not at compile time! We call this reification process. Subsequently, the runtime remembers this result, so it does not have to re-create it again. This part is called memoization .
Compare with C ++, which does not require a JIT compiler as part of its runtime. The C ++ compiler really needs to evaluate superfunctions (called "templates") at compile time. This is a possible option because the arguments to superfunctions are limited to things that can be evaluated at compile time.
So, to answer your question:
class Foo { public void Bar() { } }
Foo is a regular type, and only one of them. Bar is a regular method inside Foo and there is only one of them.
class Foo<T> { public void Bar() { } }
Foo<T> is a superfunction that creates types at runtime. Each of these resulting types has its own regular method named Bar and only one of them (for each type).
class Foo { public void Bar<T>() { } }
Foo is a regular type, and only one of them. Bar<T> is a superfunction that creates regular methods at runtime. Each of these resulting methods will then be considered part of the regular Foo type.
class Foo<Τ1> { public void Bar<T2>() { } }
Foo<T1> is a superfunction that creates types at runtime. Each of these resulting types has its own superfunction called Bar<T2> , which creates regular methods at runtime (later). Each of these resulting methods is considered part of the type that created the corresponding superfunction.
The above is a conceptual explanation. In addition, some optimizations can be implemented to reduce the number of different implementations in memory - for example, two constructed methods can share a single implementation of machine code under certain circumstances. See the Luaan answer about why the CLR can do this and when it really does.