I\'ve read some information about generics in .ΝΕΤ and noticed one interesting thing.
For example, if I have a generic class:
class Foo
{
As opposed to C++ templates, .NET generics are evaluated in runtime, not at compile-time. Semantically, if you instantiate the generic class with different type parameters, those will behave as if it were two different classes, but under the hood, there is only one class in the compiled IL (intermediate language) code.
The difference between different instantiatons of the same generic type becomes apparent when you use Reflection: typeof(YourClass<int>)
will not be the same as typeof(YourClass<string>)
. These are called constructed generic types. There also exists a typeof(YourClass<>)
which represents the generic type definition. Here are some further tips on dealing with generics via Reflection.
When you instantiate a constructed generic class, the runtime generates a specialized class on the fly. There are subtle differences between how it works with value and reference types.
For generic methods, the principles are the same.
In IL itself, there's just one "copy" of the code, just like in C#. Generics are fully supported by IL, and the C# compiler doesn't need to do any tricks. You will find that each reification of a generic type (e.g. List<int>
) has a separate type, but they still keep a reference to the original open generic type (e.g. List<>
); however, at the same time, as per contract, they must behave as if there were separate methods or types for each closed generic. So the simplest solution is indeed to have each closed generic method be a separate method.
Now for the implementation details :) In practice, this is rarely necessary, and can be expensive. So what actually happens is that if a single method can handle multiple type arguments, it will. This means that all reference types can use the same method (the type safety is already determined at compile-time, so there's no need to have it again in runtime), and with a little trickery with static fields, you can use the same "type" as well. For example:
class Foo<T>
{
private static int Counter;
public static int DoCount() => Counter++;
public static bool IsOk() => true;
}
Foo<string>.DoCount(); // 0
Foo<string>.DoCount(); // 1
Foo<object>.DoCount(); // 0
There's only one assembly "method" for IsOk
, and it can be used by both Foo<string>
and Foo<object>
(which of course also means that calls to that method can be the same). But their static fields are still separate, as required by the CLI specification, which also means that DoCount
must refer to two separate fields for Foo<string>
and Foo<object>
. And yet, when I do the disassembly (on my computer, mind you - these are implementation details and may vary quite a bit; also, it takes a bit of effort to prevent the inlining of DoCount
), there's only one DoCount
method. How? The "reference" to Counter
is indirect:
000007FE940D048E mov rcx, 7FE93FC5C18h ; Foo<string>
000007FE940D0498 call 000007FE940D00C8 ; Foo<>.DoCount()
000007FE940D049D mov rcx, 7FE93FC5C18h ; Foo<string>
000007FE940D04A7 call 000007FE940D00C8 ; Foo<>.DoCount()
000007FE940D04AC mov rcx, 7FE93FC5D28h ; Foo<object>
000007FE940D04B6 call 000007FE940D00C8 ; Foo<>.DoCount()
And the DoCount
method looks something like this (excluding the prolog and "I don't want to inline this method" filler):
000007FE940D0514 mov rcx,rsi ; RCX was stored in RSI in the prolog
000007FE940D0517 call 000007FEF3BC9050 ; Load Foo<actual> address
000007FE940D051C mov edx,dword ptr [rax+8] ; EDX = Foo<actual>.Counter
000007FE940D051F lea ecx,[rdx+1] ; ECX = RDX + 1
000007FE940D0522 mov dword ptr [rax+8],ecx ; Foo<actual>.Counter = ECX
000007FE940D0525 mov eax,edx
000007FE940D0527 add rsp,30h
000007FE940D052B pop rsi
000007FE940D052C ret
So the code basically "injected" the Foo<string>
/Foo<object>
dependency, so while the calls are different, the method being called is actually the same - just with a bit more indirection. Of course, for our original method (() => Counter++
), this will not be a call at all, and will not have the extra indirection - it will just inline in the callsite.
It's a bit trickier for value types. Fields of reference types are always the same size - the size of the reference. On the other hand, fields of value types may have different sizes e.g. int
vs. long
or decimal
. Indexing an array of integers requires different assembly than indexing an array of decimal
s. And since structs can be generic too, the size of the struct may depend on the size of the type arguments:
struct Container<T>
{
public T Value;
}
default(Container<double>); // Can be as small as 8 bytes
default(Container<decimal>); // Can never be smaller than 16 bytes
If we add value types to our earlier example
Foo<int>.DoCount();
Foo<double>.DoCount();
Foo<int>.DoCount();
We get this code:
000007FE940D04BB call 000007FE940D00F0 ; Foo<int>.DoCount()
000007FE940D04C0 call 000007FE940D0118 ; Foo<double>.DoCount()
000007FE940D04C5 call 000007FE940D00F0 ; Foo<int>.DoCount()
As you can see, while we don't get the extra indirection for the static fields unlike with the reference types, each method is actually entirely separate. The code in the method is shorter (and faster), but cannot be reused (this is for Foo<int>.DoCount()
:
000007FE940D058B mov eax,dword ptr [000007FE93FC60D0h] ; Foo<int>.Counter
000007FE940D0594 lea edx,[rax+1]
000007FE940D0597 mov dword ptr [7FE93FC60D0h],edx
Just a plain static field access as if the type wasn't generic at all - as if we just defined class FooOfInt
and class FooOfDouble
.
Most of the time, this isn't really important for you. Well-designed generics usually more than pay for their costs, and you can't just make a flat statement about the performance of generics. Using a List<int>
will almost always be a better idea than using ArrayList
of ints - you pay the extra memory cost of having multiple List<>
methods, but unless you have many different value-type List<>
s with no items, the savings will likely well outweigh the cost in both memory and time. If you only have one reification of a given generic type (or all the reifications are closed on reference types), you usually aren't going to pay extra - there may be a bit of extra indirection if inlining isn't possible.
There's a few guidelines to using generics efficiently. The most relevant here is to only keep the actually generic parts generic. As soon as the containing type is generic, everything inside may also be generic - so if you have 100 kiB of static fields in a generic type, every reification will need to duplicate that. This may be what you want, but it might be a mistake. The usual aproach is to put the non-generic parts in a non-generic static class. The same applies to nested classes - class Foo<T> { class Bar { } }
means that Bar
is also a generic class (it "inherits" the type argument of its containing class).
On my computer, even if I keep the DoCount
method free of anything generic (replace Counter++
with just 42
), the code is still the same - the compilers don't try to eliminate unnecessary "genericity". If you need to use a lot of different reifications of one generic type, this can add up quickly - so do consider keeping those methods apart; putting them in a non-generic base class or a static extension method might be worthwhile. But as always with performance - profile. It probably isn't an issue.
First off, let's clarify two things. This is a generic method definition:
T M<T>(T x)
{
return x;
}
This is a generic type definition:
class C<T>
{
}
Most likely, if I ask you what M
is, you'll say that it's a generic method that takes a T
and returns a T
. That's absolutely correct, but I propose a different way of thinking about it -- there are two sets of parameters here. One is the type T
, the other is the object x
. If we combine them, we know that collectively this method takes two parameters in total.
The concept of currying tells us that a function that takes two parameters can be transformed to a function that takes one parameter and returns another function that takes the other parameter (and vice versa). For example, here's a function that takes two integers and produces their sum:
Func<int, int, int> uncurry = (x, y) => x + y;
int sum = uncurry(1, 3);
And here's an equivalent form, where we have a function that takes one integer and produces a function that takes another integer and returns the sum of those aforementioned integers:
Func<int, Func<int, int>> curry = x => y => x + y;
int sum = curry(1)(3);
We went from having one function that takes two integers to having a function that takes an integer and creates functions. Obviously, these two aren't literally the same thing in C#, but they are two different ways of saying the same thing, because passing the same information will eventually get you to the same final result.
Currying allows us to reason about functions easier (it's easier to reason about one parameter than two) and it allow 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 takes place here. Let's say M
is a "super-function" that takes a type T
and returns a regular method. That returned method takes a T
value and returns a T
value.
For example, if we call the super-function M
with the argument int
, we get a regular method from int
to int
:
Func<int, int> e = M<int>;
And if we call that regular method with the argument 5
, we get a 5
back, as we expected:
int v = e(5);
So, consider the following expression:
int v = M<int>(5);
Do you see now why this could be considered as two separate calls? You can recognize the call to the super-function because its arguments are passed in <>
. Then the call to the returned method follows, where the arguments are passed in ()
. It's analogous to the previous example:
curry(1)(3);
And similarly, a generic type definition is also a super-function that takes a type and returns another type. For example, List<int>
is a call to the super-function List
with an argument int
that returns a type that's a list of integers.
Now when the C# compiler meets a regular method, it compiles it as a regular method. It doesn't attempt to create different definitions for different possible arguments. So, this:
int Square(int x) => x * x;
gets compiled as it is. It does not get compiled as:
int Square__0() => 0;
int Square__1() => 1;
int Square__2() => 4;
// and so on
In other words, the C# compiler does not evaluate all possible arguments for this method in order to embed them into the final exacutable -- rather, it leaves the method in its parameterized form and trusts that the result will be evaluated at runtime.
Similarly, when the C# compiler meets a super-function (a generic method or type definition), it compiles it as a super-function. It doesn't attempt to create different definitions for different possible arguments. So, this:
T M<T>(T x) => x;
gets compiled as it is. It does not get compiled 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 super-function is called, it will be evaluated at runtime, and the regular method or type will be produced by that evaluation.
This is one of the reasons why C# is benefitted from having a JIT-compiler as part of its runtime. When a super-function is evaluated, it produces a brand new method or a type that wasn't there at compile time! We call that process reification. Subsequently, the runtime remembers that result so it won't have to re-create it again. That part is called memoization.
Compare with C++ which doesn't require a JIT-compiler as part of its runtime. The C++ compiler actually needs to evaluate the super-functions (called "templates") at compile time. That's a feasible option because the arguments of the super-functions are restricted 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 there's only one of it. Bar
is a regular method inside Foo
and there's only one of it.
class Foo<T>
{
public void Bar()
{
}
}
Foo<T>
is a super-function that creates types at runtime. Each one of those resulting types has its own regular method named Bar
and there's only one of it (for each type).
class Foo
{
public void Bar<T>()
{
}
}
Foo
is a regular type and there's only one of it. Bar<T>
is a super-function that creates regular methods at runtime. Each one of those resulting methods will then be considered part of the regular type Foo
.
class Foo<Τ1>
{
public void Bar<T2>()
{
}
}
Foo<T1>
is a super-function that creates types at runtime. Each one of those resulting types has its own a super-function named Bar<T2>
that creates regular methods at runtime (at a later time). Each one of those resulting methods is considered part of the type that created the corresponding super-function.
The above is the conceptual explanation. Beyond it, certain optimizations can be implemented to reduce the number of distinct implementations in memory -- e.g. two constructed methods can share a single machine-code implementation under certain circumstances. See Luaan's answer about why the CLR can do this and when it actually does it.